Traefik forwardAuth explained
From a picture above it should be pretty clear how it works.
In general, points are:
- before proceeding to our service, request is send to auth service
- depending on auth service response Traefik will either continue or block request
Before going deeper let's build and sandbox to see it in action
For demo purposes we are going to run everything in docker, so no need to install anything
First, let's start some service (aka the app we are going to protect)
docker run -it --rm --name=echo ealen/echo-server
Note: this one is an simple echo server, that will log and respond with details about incomming request, listening on port 80, we are not exposing it by intent
Second, for an auth service, actually, let's run the same image:
docker run -it --rm --name=auth ealen/echo-server
Note: the only difference here is the container name, we are going to use it in next step
Now it is turn for Traefik, first prepare configuration files:
static.yml
log:
level: DEBUG
accessLog:
format: common
providers:
file:
filename: /etc/traefik/dynamic.yml
entryPoints:
http:
address: :8080
asDefault: true
experimental:
plugins:
google-oidc-auth-middleware:
moduleName: github.com/andrewkroh/google-oidc-auth-middleware
version: v0.1.0
dynamic.yml
http:
middlewares:
auth:
forwardAuth:
address: http://auth:80
routers:
echo:
rule: Path(`/`)
service: echo
middlewares:
- auth
services:
echo:
loadBalancer:
servers:
- url: http://echo:80/
And finally run traefik:
docker run -it --rm --name=traefik --link=echo --link=auth -p 8080:8080 -v "$PWD/static.yml:/etc/traefik/static.yml" -v "$PWD/dynamic.yml:/etc/traefik/dynamic.yml" traefik:v3 --configfile=/etc/traefik/static.yml
Note: here we are linking
echo
andauth
containers, so Traefik can reach them
Now, give it a try, and try call localhost:8080
curl localhost:8080
Note how both auth
and echo
containers receive the request
- Once received an request, Traefik did send request to our
auth
container - It did respond with 200 OK and details about request
- Once received an 200 OK response from
auth
service, Traefik continued and proxied request toecho
echo
did respond with 200 OK and that response was proxied back to us
If you make few more calls, you should confirm, that Traefik does repeat that for all requests. This one may be important, if you are working with services under high load.
Also, if you have closer look you should notice that details about original request are passed as x-forwarded-
headers, most of all, we probably interested in:
x-forwarded-host
- which will contain hostname, and optionally portx-forwarded-uri
- will container path with query string
Cookies and authorization headers are passed as is, so you can access them as usual
Dummy auth service for Traefikd forward auth
So why not have fun, and build some dummy auth service
Here is an example of echo like service to start from
import { createServer } from 'http'
createServer((req, res) => {
console.log({
method: req.headers['x-forwarded-method'],
host: req.headers['x-forwarded-host'],
uri: req.headers['x-forwarded-uri'],
})
return res.writeHead(200, { 'Content-Type': 'text/plain' }).end('OK\n')
}).listen(3000, () => console.log('open http://localhost:3000'))
Start it like so:
docker run -it --rm --name=auth -v "$PWD/echo.js:/app/echo.js" -w /app node:23-alpine node echo.js
Note, because we are listening for port 3000, we should modify our dynamic.yml
like so:
http:
middlewares:
auth:
forwardAuth:
address: http://auth:3000
# 👆 we have changed port here, everything else is the same
routers:
echo:
rule: Path(`/`)
service: echo
middlewares:
- auth
services:
echo:
loadBalancer:
servers:
- url: http://echo:80/
restart Traefik
docker run -it --rm --name=traefik --link=echo --link=auth -p 8080:8080 -v "$PWD/static.yml:/etc/traefik/static.yml" -v "$PWD/dynamic.yml:/etc/traefik/dynamic.yml" traefik:v3 --configfile=/etc/traefik/static.yml
make curl call:
curl -X POST 'http://localhost:8080/?foo=bar'
note how you are receing response from echo server, while in auth service we are logging following:
{ method: 'POST', host: 'localhost:8080', uri: '/?foo=bar' }
Here is our dummy part, let's check user
query string parameter, if it is present - we are treating request as valid, and responding with 200 OK, if not, respond with 401
import { createServer } from 'http'
createServer((req, res) => {
const user = new URL(req.headers['x-forwarded-uri'], 'http://localhost').searchParams.get('user')
console.log({
method: req.headers['x-forwarded-method'],
host: req.headers['x-forwarded-host'],
uri: req.headers['x-forwarded-uri'],
user: user,
})
if (!user) {
return res.writeHead(401, { 'Content-Type': 'text/plain' }).end('Who are you?\n')
}
return res.writeHead(200, { 'Content-Type': 'text/plain' }).end('OK\n')
}).listen(3000, () => console.log('open http://localhost:3000'))
Now attemt to run:
curl -X POST 'http://localhost:8080/?foo=bar'
will fail with 401 Unauthorized error and response Who are you?
and request like this:
curl -X POST 'http://localhost:8080/?user=alice'
will succeed.
Technically, that all, and yes, it is so simple.
Buf ofcourse noone will use such implementation, at the very end we need cookies to handle the state, here is another minimalistic example:
import { createServer } from 'http'
createServer((req, res) => {
const url = new URL(req.headers['x-forwarded-proto'] + '://' + req.headers['x-forwarded-host'] + req.headers['x-forwarded-uri'])
// let's pretend such url is called on OAuth flow completion callback, from here we want to set an cookie and redirect user to home page
if (url.pathname === '/demo-as-if-it-was-callback-from-oauth' && url.searchParams.get('user')) {
return res.writeHead(302, { 'Set-Cookie': `user=${url.searchParams.get('user')}; HttpOnly;`, Location: req.headers['x-forwarded-proto'] + '://' + req.headers['x-forwarded-host'] + '/' }).end()
}
const user = req.headers.cookie?.match(/user=(\w+)/)?.pop()
console.log({
url: url.toString(),
user: user,
})
if (!user) {
return res.writeHead(401, { 'Content-Type': 'text/plain' }).end('Who are you?\n')
}
return res.writeHead(200, { 'Content-Type': 'text/plain' }).end('OK\n')
}).listen(3000, () => console.log('open http://localhost:3000'))
keep an eye on how we are handing very special endpoint
let's check if everything still works
curl 'http://localhost:8080/?user=alice'
as expected gives us "Who are you?" response
Now, let's pretend we have logged in:
curl -s -i 'http://localhost:8080/demo-as-if-it-was-callback-from-oauth?user=alice'
we will receive 404 - which is indeed expected, we need to modify our router a little bit
http:
middlewares:
auth:
forwardAuth:
address: http://auth:3000
routers:
echo:
rule: Path(`/`) || Path(`/demo-as-if-it-was-callback-from-oauth`)
# 👆 we have added one more path to this route, later we will see other options
service: echo
middlewares:
- auth
services:
echo:
loadBalancer:
servers:
- url: http://echo:80/
now our request gives us desired response:
HTTP/1.1 302 Found
Date: Fri, 14 Mar 2025 08:03:44 GMT
Location: http://localhost:8080/
Set-Cookie: user=alice; HttpOnly;
Content-Length: 0
and as you may guess next request from browser will be:
curl 'http://localhost:8080/?foo=bar' -H 'Cookie: user=alice'
which will work
at this point you should be able to do the same in browser by opening this urls
Note: in real Traefik setups your routes will be catching hostanemes, aka:
http:
routers:
echo:
rule: Host(`echo.mac-blog.org.ua`)
Because of that, they will catch all requests and there will be no need for dedicated rule to catch this callback url
Traefik OpenId Connect forwardAuth
Of course, no one will rely on such dummy auth service.
To bypass all examples in the middle let's just directly into oidc
It is required to have some understanding of how it works, previously there were some notes like this:
- OAuth2 Proxy in Kubernetes
- OAuth 2.0 and OpenID Connect journey
- Golang OIDC verifier for Azure and Github Actions
But never the less here is what we are going to build:
Before jumping into complicated suff let's play with Github, it has minimalistic OAuth implementation
Traefik forwardAuth Github
Navigate to github.com/settings/developers and create new app with following params:
- Application name: Traefik
- Homepage URL: http://localhost:8080
- Authorization callback URL: http://localhost:8080/can-be-anything-we-want
Note: because literally all requests are catched and send to our service, we may use anything we want as callback url, for demo purposes I have choosen /can-be-anything-we-want
to highlight that
Once created, press "Generate a new client secret" button
And keep note on both client_id
and client_secret
in my case values are:
- client_id: Ov23liWuoZki5P2ubkFy
- client_secret: b23abc4e88d9e3973d5ac199dc4844c3b9f675ce
How it will work:
on auth side, we are going to check github
cookie
if request has it and it is "valid" then respond with 200 ok otherwise we need to block request
to keep everything secure, we are going to use session cookies
note: there are two use cases - web requests and api requests we should behave differently for them
here is naive pseudo code for auth middleware:
async function handler(req, res) {
if (req.headers['x-forwarded-uri'].startsWith('/can-be-anything-we-want')) {
const code = extractCodeFromQueryString(req.headers['x-forwarded-uri'])
const token = await exchangeCodeToToken(code)
return res.redirect('/', { 'set-cookie': `github=${token}` })
}
const tokenFromCookie = extractGithubCookieFrom(req)
const valid = await callUserInfoEndpointWith(tokenFromCookie)
if (valid) {
return res.sendStatus(200)
}
if (isApiRequest(req)) {
return res.sendStatus(401)
} else {
return res.redirect(buildGithubLoginUrl())
}
}
it is kind of similar to previous example with cookies
here is what i have ended up with
import { createServer } from 'http'
createServer(async (req, res) => {
const url = new URL(req.headers['x-forwarded-proto'] + '://' + req.headers['x-forwarded-host'] + req.headers['x-forwarded-uri'])
console.log('processing', url.toString())
// this one is where user redirected after github login
if (url.pathname === '/can-be-anything-we-want' && url.searchParams.get('code')) {
const client_id = 'Ov23liWuoZki5P2ubkFy'
const client_secret = 'b23abc4e88d9e3973d5ac199dc4844c3b9f675ce'
const code = url.searchParams.get('code')
console.log(`callback, exchanging code ${code} for token`)
const { access_token } = await fetch('https://github.com/login/oauth/access_token', {
method: 'POST',
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
body: JSON.stringify({ client_id, client_secret, code }),
}).then(r => r.json())
console.log(`got token ${access_token}, set it as github cookie and redirect user to home page`)
return res.writeHead(302, { 'Set-Cookie': `github=${access_token}; HttpOnly;`, Location: req.headers['x-forwarded-proto'] + '://' + req.headers['x-forwarded-host'] + '/' }).end()
}
const github = req.headers.cookie?.match(/github=(\w+)/)?.pop()
if (!github) {
console.log('not authenticated, redirect user to github login')
return res.writeHead(302, { Location: getLoginUrl() }).end()
}
console.log('authenticated, lets check email')
var { email } = await fetch('https://api.github.com/user', { headers: { Authorization: `token ${github}` } }).then(r => r.json())
// if (email !== '[email protected]') {
// console.log(`unexpected email ${email}, redirect user to github login`)
// return res.writeHead(302, { Location: getLoginUrl() }).end()
// }
console.log(`email: ${email}`)
return res.writeHead(200, { 'Content-Type': 'text/plain' }).end('OK\n')
}).listen(3000, () => console.log('open http://localhost:3000'))
function getLoginUrl() {
const url = new URL('https://github.com/login/oauth/authorize')
url.searchParams.set('client_id', 'Ov23liWuoZki5P2ubkFy')
url.searchParams.set('redirect_uri', 'http://localhost:8080/can-be-anything-we-want')
url.searchParams.set('scope', ['user'].join(' '))
url.searchParams.set('state', crypto.randomUUID())
url.searchParams.set('allow_signup', 'false')
return url.toString()
}
and for it to work, we need to once again modify our route
http:
# ..
routers:
echo:
rule:
Path(`/`) || Path(`/can-be-anything-we-want`) # 👈 path
# ...
with this setup - it will work in browser:
- when you open
http://localhost:8080
- you will be redirected to
https://github.com/login/oauth/authorize?client_id=Ov23liWuoZki5P2ubkFy&redirect_uri=http%3A%2F%2Flocalhost%3A8080%2Fcan-be-anything-we-want&scope=user&state=9f8d6d03-b629-45d5-9bdc-72734a1aaa88&allow_signup=false
- once logged in, we will be redirected back to
http://localhost:8080/can-be-anything-we-want?code=4aa78194e6eb9314ca9a&state=5e8302fd-22a6-44cf-bd27-e08f26bb4f89
- after exchanging
code
to token, and setting it as angithub
cookie we will redirect user back to home page, so browser will be landed tohttp://localhost:8080
- once
http://localhost:8080
is requested once again, our middleware does recognize the cookie and checks its email, returns 200 OK so Traefik proceeds and proxies our request to echo service
Note, that on each subsequent request our auth service is still called and still performs checks which both good and bad, depending on what we are protecting
Just from curiosity, lets add some caching and encryption on top
Here is the idea:
- once authenticated, we will store authentication details in memory, tracking last time we did check them
- for cookie, instead of saving github token we will save our own encrypted session token
- for subsequent requests we will check our in memory storage instead of calling github api each time
Here is example implementation
import { createServer } from 'http'
import { createHash, createCipheriv, createDecipheriv, randomBytes, randomUUID } from 'crypto'
const secret = 'SecretPhraseToEncryptDecryptCookie'
const memory = new Map()
const client_id = 'Ov23liWuoZki5P2ubkFy'
const client_secret = 'b23abc4e88d9e3973d5ac199dc4844c3b9f675ce'
const redirect_uri = 'http://localhost:8080/can-be-anything-we-want'
createServer(async (req, res) => {
const url = new URL(req.headers['x-forwarded-proto'] + '://' + req.headers['x-forwarded-host'] + req.headers['x-forwarded-uri'])
console.log('processing', url.toString())
// this one is where user redirected after github login
if (url.pathname === '/can-be-anything-we-want' && url.searchParams.get('code')) {
const code = url.searchParams.get('code')
console.log(`callback, exchanging code ${code} for token`)
const { access_token } = await fetch('https://github.com/login/oauth/access_token', {
method: 'POST',
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
body: JSON.stringify({ client_id, client_secret, code }),
}).then(r => r.json())
console.log(`got an access token, retrieving user details`)
const info = await getUserDetails(access_token)
console.log(info)
if (!info) {
console.log('something wrong, can not retrieve user info, responding with 401')
return res.writeHead(401, { 'Content-Type': 'text/plain' }).end('Who are you?\n')
}
memory.set(info.id, info)
const session = encrypt(info.id, secret)
console.log('session', session)
console.log(`redirecting user back to home page with session cookie`)
return res.writeHead(302, { 'Set-Cookie': `session=${session}; HttpOnly;`, Location: req.headers['x-forwarded-proto'] + '://' + req.headers['x-forwarded-host'] + '/' }).end()
}
const session = req.headers.cookie?.match(/session=([^(;|$)]+)/)?.pop()
if (!session) {
console.log('not authenticated, redirect user to github login')
return res.writeHead(302, { Location: getLoginUrl() }).end()
}
console.log('incomming session', session)
const id = decrypt(session, secret)
if (!id) {
console.log('invalid session, can not decrypt, redirect user to github login')
return res.writeHead(302, { Location: getLoginUrl() }).end()
}
const info = memory.get(Number(id))
if (!info) {
console.log('invalid session, no user info found, redirect user to github login')
return res.writeHead(302, { Location: getLoginUrl() }).end()
}
const timePassedSinceLastCheck = Date.now() - info.at
if (timePassedSinceLastCheck > 60 * 60 * 1000) {
console.log('token was checked more than 1 hour ago, we should recheck it once again')
const info = await getUserDetails(info.token)
if (!info) {
console.log('failed to recheck token, redirect user to github login')
return res.writeHead(302, { Location: getLoginUrl() }).end()
}
console.log(info)
memory.set(info.id, info)
}
console.log(info)
console.log('responding with 200 OK to Traefik, passing additonal headers')
return res
.writeHead(200, {
'Content-Type': 'text/plain',
'X-User-Id': info.id,
'X-User-Login': info.login,
'X-User-Name': info.name,
'X-User-Avatar': info.avatar_url,
})
.end('OK\n')
}).listen(3000, () => console.log('open http://localhost:3000'))
function getLoginUrl() {
const url = new URL('https://github.com/login/oauth/authorize')
url.searchParams.set('client_id', client_id)
url.searchParams.set('redirect_uri', redirect_uri)
url.searchParams.set('scope', ['user'].join(' '))
url.searchParams.set('state', randomUUID())
url.searchParams.set('allow_signup', 'false')
return url.toString()
}
async function getUserDetails(token) {
// { "id": 88868, "login": "mac2000", "name": "Alexandr Marchenko", "email": "[email protected]", "avatar_url": "https://avatars.githubusercontent.com/u/88868?v=4" }
const { id, login, name, avatar_url } = await fetch('https://api.github.com/user', { headers: { Authorization: `Bearer ${token}` } })
.then(r => r.json())
.catch(error => {
console.warn(`error while retrieving user info ${error.message}`)
return {}
})
if (!id) {
return null
}
return { id, login, name, avatar_url, token, at: Date.now() }
}
function encrypt(text, secret) {
if (typeof text === 'number') {
text = text.toString()
}
const key = createHash('sha256').update(secret).digest('hex').substring(0, 32)
const iv = randomBytes(16)
const cipher = createCipheriv('aes-256-ctr', key, iv)
const bytes = Buffer.concat([cipher.update(text, 'utf8'), cipher.final()])
return iv.toString('base64') + ':' + bytes.toString('base64')
}
function decrypt(input, secret) {
try {
const key = createHash('sha256').update(secret).digest('hex').substring(0, 32)
const [iv, encrypted] = input.split(':')
const decipher = createDecipheriv('aes-256-ctr', key, Buffer.from(iv, 'base64'))
const bytes = Buffer.concat([decipher.update(encrypted, 'base64'), decipher.final()])
return bytes.toString('utf-8')
} catch (error) {
console.warn(`error while decrypting given input ${error.message}`)
return null
}
}
Notes:
- we are storin in memory map where key is user id, and value are user details
- for cookie we are storing encrypted user id
- every hour, just in case, we are revalidating token
Also, for demo purposes, we are passing headers like X-User-Login
to our echo service
for this to work, add following changes to dynamic.yml
http:
middlewares:
auth:
forwardAuth:
address: http://auth:3000
authResponseHeaders:
- X-User-Id
- X-User-Login
- X-User-Name
- X-User-Avatar
# ...
so we have almost ready to use example for github, add few more checks here and there and you are reday to go
but initially the goal was to cover oidc forward auth so
Traefik Google auth
Now, when we have played with simplified Github example, it should be much easier to do something similar with Google
By intent I will start with nodejs example, so it can be run as single script without the need to compile anything, and later on, if everything will be working fine, will add Golang example as well
For Google we gonna need Google Cloud Project, configured OAuth contest screen and create an client - I expect this one should not be a problem, otherwise everthing below will be an dark magic
here is what i have ended up with
import { createServer } from 'http'
import { createHash, createCipheriv, createDecipheriv, randomBytes, randomUUID } from 'crypto'
const secret = 'SecretPhraseToEncryptDecryptCookie'
const memory = new Map()
const client_id = '869226817339-0hlu97qq43ia4l24eugbl3q570v373m7.apps.googleusercontent.com'
const client_secret = 'GOCSPX-xxxxxx-kJsu'
const redirect_uri = 'http://localhost:8080/callback'
createServer(async (req, res) => {
const url = new URL(req.headers['x-forwarded-proto'] + '://' + req.headers['x-forwarded-host'] + req.headers['x-forwarded-uri'])
console.log('processing', url.toString())
// this one is where user redirected after google login
if (url.pathname === '/callback' && url.searchParams.get('code')) {
const code = url.searchParams.get('code')
console.log(`callback, exchanging code ${code} for token`)
const { access_token } = await fetch('https://oauth2.googleapis.com/token', {
method: 'POST',
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
body: JSON.stringify({
grant_type: 'authorization_code',
code,
client_id,
client_secret,
redirect_uri,
}),
}).then(r => r.json())
console.log(`got an access token, retrieving user details`)
const info = await getUserDetails(access_token)
console.log(info)
if (!info) {
console.log('something wrong, can not retrieve user info, responding with 401')
return res.writeHead(401, { 'Content-Type': 'text/plain' }).end('Who are you?\n')
}
memory.set(info.id, info)
console.log('memory keys', Array.from(memory.keys()))
console.log('id', info.id)
const session = encrypt(info.id, secret)
console.log('session', session)
console.log(`redirecting user back to home page with session cookie`)
return res.writeHead(302, { 'Set-Cookie': `session=${session}; HttpOnly;`, Location: req.headers['x-forwarded-proto'] + '://' + req.headers['x-forwarded-host'] + '/' }).end()
}
const session = req.headers.cookie?.match(/session=([^(;|$)]+)/)?.pop()
if (!session) {
console.log('not authenticated, redirect user to google login')
return res.writeHead(302, { Location: getLoginUrl() }).end()
}
console.log('incomming session', session)
const id = decrypt(session, secret)
console.log('decrypted id', id)
if (!id) {
console.log('invalid session, can not decrypt, redirect user to google login')
return res.writeHead(302, { Location: getLoginUrl() }).end()
}
console.log('memory keys', Array.from(memory.keys()))
const info = memory.get(id)
if (!info) {
console.log('invalid session, no user info found, redirect user to google login')
return res.writeHead(302, { Location: getLoginUrl() }).end()
}
const timePassedSinceLastCheck = Date.now() - info.at
if (timePassedSinceLastCheck > 60 * 60 * 1000) {
console.log('token was checked more than 1 hour ago, we should recheck it once again')
const info = await getUserDetails(info.token)
if (!info) {
console.log('failed to recheck token, redirect user to google login')
return res.writeHead(302, { Location: getLoginUrl() }).end()
}
console.log(info)
memory.set(info.id, info)
}
console.log(info)
console.log('responding with 200 OK to Traefik, passing additonal headers')
return res
.writeHead(200, {
'Content-Type': 'text/plain',
'X-User-Id': info.id,
'X-User-Name': info.name,
'X-User-Picture': info.picture,
})
.end('OK\n')
}).listen(3000, () => console.log('open http://localhost:3000'))
function getLoginUrl() {
const url = new URL('https://accounts.google.com/o/oauth2/v2/auth')
url.searchParams.set('response_type', 'code')
url.searchParams.set('client_id', client_id)
url.searchParams.set('redirect_uri', redirect_uri)
url.searchParams.set('scope', ['profile'].join(' '))
return url.toString()
}
async function getUserDetails(token) {
// { "sub": "114259064191790696813", "name": "Alexandr Marchenko", "given_name": "Alexandr", "family_name": "Marchenko", "picture": "https://lh3.googleusercontent.com/a/ACg8ocLCg6rfbaPrEuotvuKegwXSu-TAXlT20AfJNJuoj5cZx604sxca\u003ds96-c" }
const { sub, name, picture } = await fetch('https://openidconnect.googleapis.com/v1/userinfo', { headers: { Authorization: `Bearer ${token}` } })
.then(r => r.json())
.catch(error => {
console.warn(`error while retrieving user info ${error.message}`)
return {}
})
if (!sub) {
return null
}
return { id: sub, name, picture, token, at: Date.now() }
}
function encrypt(text, secret) {
const key = createHash('sha256').update(secret).digest('hex').substring(0, 32)
const iv = randomBytes(16)
const cipher = createCipheriv('aes-256-ctr', key, iv)
const bytes = Buffer.concat([cipher.update(text, 'utf8'), cipher.final()])
return iv.toString('base64') + ':' + bytes.toString('base64')
}
function decrypt(input, secret) {
try {
const key = createHash('sha256').update(secret).digest('hex').substring(0, 32)
const [iv, encrypted] = input.split(':')
const decipher = createDecipheriv('aes-256-ctr', key, Buffer.from(iv, 'base64'))
const bytes = Buffer.concat([decipher.update(encrypted, 'base64'), decipher.final()])
return bytes.toString('utf-8')
} catch (error) {
console.warn(`error while decrypting given input ${error.message}`)
return null
}
}
note: in real life we want to utilize refresh tokens and do some fancy stuff, but in my case it is expected to have short lived session cookies - aka it will last only whily you are using the app, and once you close the browser - you are done, next time - new login
So the final setup is:
static.yml
log:
level: DEBUG
accessLog:
format: common
providers:
file:
filename: /etc/traefik/dynamic.yml
entryPoints:
http:
address: :8080
asDefault: true
dynamic.yml
http:
middlewares:
auth:
forwardAuth:
address: http://auth:3000
authResponseHeaders:
- X-User-Id
- X-User-Name
- X-User-Picture
routers:
echo:
rule: Path(`/`) || Path(`/callback`)
service: echo
middlewares:
- auth
services:
echo:
loadBalancer:
servers:
- url: http://echo:80/
echo
docker run -it --rm --name=echo ealen/echo-server
auth
docker run -it --rm --name=auth -v "$PWD/google.js:/app/google.js" -w /app node:23-alpine node google.js
traefik
docker run -it --rm --name=traefik --link=echo --link=auth -p 8080:8080 -v "$PWD/static.yml:/etc/traefik/static.yml" -v "$PWD/dynamic.yml:/etc/traefik/dynamic.yml" traefik:v3 --configfile=/etc/traefik/static.yml
OAuth2-proxy Traefik forwardAuth
Having all that in place, should allow us to not reinwent the wheel and configure something existant, for example oauth2-proxy
Technically it does exactly the same we were writting before
Here is small refresher - OAuth2 Proxy in Kubernetes
docker run -it --rm --name=auth -p 4180:4180 quay.io/oauth2-proxy/oauth2-proxy \
--provider=google \
--client-id=869226817339-c14i5aj3mq1rkht381u9c15lujm5810b.apps.googleusercontent.com \
--client-secret=GOCSPX-xxxxxx \
--cookie-secret=wsFX9kYoi00jYjA5m_tv6hzZvwk8OvHmtW9pCiQkpXA= \
--email-domain=* \
--upstream=file:///dev/null \
--redirect-url=http://localhost:4180/oauth2/callback \
--http-address=0.0.0.0:4180 \
--cookie-secure=false
Notes:
- to create cookie seecret use snippets from docs - under the hood 32 random bytes
- while in localhost - we disabling secure flag for cookies
- you should be able to open
http://localhost:4180
and login via Google - after login, you will be landed on 404 page not found - which is fine for now
There is an documentation and examples of how to integrate Traefik with OAuth2 here
After reading docs many times I have ended up with following setup:
echo
docker run -it --rm --name=echo ealen/echo-server
auth
docker run -it --rm --name=auth quay.io/oauth2-proxy/oauth2-proxy \
--provider=google \
--client-id=869226817339-c14i5aj3mq1rkht381u9c15lujm5810b.apps.googleusercontent.com \
--client-secret=GOCSPX-xxxxxx \
--cookie-secret=wsFX9kYoi00jYjA5m_tv6hzZvwk8OvHmtW9pCiQkpXA= \
--email-domain=* \
--http-address=0.0.0.0:4180 \
--cookie-secure=false \
--cookie-name=auth \
--set-xauthrequest=true
Notes:
--cookie-secure=false
- for now, while playing without https--cookie-name=auth
- just to control and see it--skip-jwt-bearer-tokens=true
- for api requests, should allow authorization bearer headers--set-xauthrequest=true
- will passX-Auth-Request-User
andX-Auth-Request-Email
headers to response--skip-provider-button=true
- nice to have but does not work as expected
static.yml
log:
level: DEBUG
accessLog:
format: common
providers:
file:
filename: /etc/traefik/dynamic.yml
entryPoints:
http:
address: :8080
asDefault: true
dynamic.yml
http:
middlewares:
auth:
forwardAuth:
address: http://auth:4180/oauth2/auth
authResponseHeaders:
- X-Auth-Request-User
- X-Auth-Request-Email
- X-Forwarded-User
errors:
errors:
status:
- '401-403'
service: auth
query: '/oauth2/sign_in?rd={url}'
routers:
echo:
rule: PathPrefix(`/`)
service: echo
middlewares:
- errors
- auth
auth:
rule: PathPrefix(`/oauth2/`)
service: auth
services:
echo:
loadBalancer:
servers:
- url: http://echo:80/
auth:
loadBalancer:
servers:
- url: http://auth:4180/
traefik
docker run -it --rm --name=traefik --link=echo --link=auth -p 8080:8080 -v "$PWD/static.yml:/etc/traefik/static.yml" -v "$PWD/dynamic.yml:/etc/traefik/dynamic.yml" traefik:v3 --configfile=/etc/traefik/static.yml
With this setup you will get kind of similar behaviour to what we have written
But there is even more
Only after all this, have discovered Traefik plugins
So the plugins in Traefik are kind of JavaScript for browsers, Go code is being injected and interpretated
There is already one created Google OIDC Auth
And indeed setup is as easy as:
echo
docker run -it --rm --name=echo ealen/echo-server
auth
not needed
static.yml
log:
level: DEBUG
accessLog:
format: common
providers:
file:
filename: /etc/traefik/dynamic.yml
entryPoints:
http:
address: :8080
asDefault: true
experimental:
plugins:
google-oidc-auth-middleware:
moduleName: github.com/andrewkroh/google-oidc-auth-middleware
version: v0.1.0
dynamic.yml
http:
middlewares:
auth:
plugin:
google-oidc-auth-middleware:
authorized:
emails:
- [email protected]
cookie:
secret: wsFX9kYoi00jYjA5m_tv6hzZvwk8OvHmtW9pCiQkpXA=
insecure: true
oidc:
clientID: 869226817339-c14i5aj3mq1rkht381u9c15lujm5810b.apps.googleusercontent.com
clientSecret: GOCSPX-xxxxxx
routers:
echo:
rule: PathPrefix(`/`)
service: echo
middlewares:
- auth
services:
echo:
loadBalancer:
servers:
- url: http://echo:80/
traefik
docker run -it --rm --name=traefik --link=echo -p 8080:8080 -v "$PWD/static.yml:/etc/traefik/static.yml" -v "$PWD/dynamic.yml:/etc/traefik/dynamic.yml" traefik:v3 --configfile=/etc/traefik/static.yml
And here is one more:
experimental:
plugins:
traefikoidc:
moduleName: github.com/lukaszraczylo/traefikoidc
version: v0.5.34
http:
middlewares:
auth:
plugin:
traefikoidc:
sessionEncryptionKey: e7a4fdbd-baa7-4bf3-8ae6-f4734f0471fd
providerURL: https://accounts.google.com
clientID: 869226817339-c14i5aj3mq1rkht381u9c15lujm5810b.apps.googleusercontent.com
clientSecret: GOCSPX-xxxxxx
callbackURL: /oauth2/callback
forceHTTPS: false