25 Commits

Author SHA1 Message Date
9e5994b959 Add Generic OAuth Provider (#138) 2020-06-29 21:04:42 +01:00
870724c994 Fail if there is an error retrieving the user + extra test (#142)
Previously this would fail, but permit the request, which isn't
normally what you'd want.
2020-06-29 21:02:45 +01:00
be2b4ba9f4 Remove unused user fields (#141)
These aren't actually used anywhere and can result in a parse error
if the ID field isn't a string
2020-06-29 21:01:59 +01:00
529e28d83b Add FUNDING.yml (#135) 2020-06-26 15:41:33 +01:00
2937b04fdb Add support for resource indicator to OIDC provider (#131) 2020-06-11 12:24:51 +01:00
fb8b216481 Optionally match emails against *either* whitelist or domains when both are provided (#106)
The previous behaviour would ignore domains if the whitelist parameter was provided, however if both parameters are provided then matching either is more likely the intent.
2020-06-03 14:11:59 +01:00
8b3a950162 Add logout endpoint (#107)
Add logout endpoint that clears the auth cookie + optional "logout-redirect" config option, to which, when set, the user will be redirected.
2020-06-03 14:00:47 +01:00
655eddeaf9 Add note on using auth host mode with selective auth 2020-05-26 14:55:23 +01:00
c63fd738d6 Rename selective auth + fix selective auth examples (#130) 2020-05-26 14:47:14 +01:00
00b5d9e031 standardize on 'traefik-forward-auth-secrets' for kubernetes examples (#127) 2020-05-26 14:12:26 +01:00
8902cf8735 Use Traefik v2 in README examples and links + use consistent images in examples 2020-05-23 16:42:18 +01:00
3345f8ec69 Add traefik v2 swarm examples 2020-05-23 14:43:52 +01:00
60604ad3db Always prompt user to select account on google login
This closes #103 and as discussed in that issue, hopefully fixes a
common source of error discussed in #31
2020-05-12 13:50:05 +01:00
a668454a11 Warn when using http without insecure cookie
Closes #114
2020-05-12 13:20:51 +01:00
eec62eb03a Improve logging detail and consistency
Closes #114
2020-05-11 14:42:53 +01:00
7381450015 Improve internal function docs 2020-05-11 14:42:33 +01:00
f7a94e7db9 Add traefik v2 kubernetes examples
Ref #72 #89 #92
2020-05-07 15:47:58 +01:00
f802a366de Add note on avoiding rules that might break redirect flow
Tracked in #101
2020-05-07 15:28:00 +01:00
07f9587bc1 Modify references from Universal Authentication to Global Authentication 2020-05-07 15:22:48 +01:00
1ac0ca9732 traefik v1.7 kubernetes doc fixes 2020-04-28 17:15:53 +01:00
9abf5645b7 Add kubernetes examples + better document methods of applying authentication
Closes #33
2020-04-24 14:22:29 +01:00
3a66191314 Document ARM releases on docker hub
This is now confirmed to be working so fixes #38
2020-04-23 14:35:06 +01:00
c3b4ba8244 Allow multiple cookie domains, domains and whitelists with environment variable (#98)
* comma env-delim for array flags
* tests for env-delim flags
2020-04-14 07:48:55 +01:00
b413c60d42 Update golang arm versions 2020-04-13 18:03:46 +01:00
e678a33016 Add .git to .dockerignore 2020-04-13 18:01:07 +01:00
92 changed files with 2820 additions and 171 deletions

View File

@ -1,2 +1,3 @@
example
.travis.yml
.git

2
.github/FUNDING.yml vendored Normal file
View File

@ -0,0 +1,2 @@
github: thomseddon

View File

@ -1,4 +1,4 @@
FROM golang:1.12-alpine as builder
FROM golang:1.13-alpine as builder
# Setup
RUN mkdir -p /go/src/github.com/thomseddon/traefik-forward-auth

View File

@ -1,4 +1,4 @@
FROM golang:1.12-alpine as builder
FROM golang:1.13-alpine as builder
# Setup
RUN mkdir -p /go/src/github.com/thomseddon/traefik-forward-auth

View File

@ -2,4 +2,7 @@
format:
gofmt -w -s internal/*.go internal/provider/*.go cmd/*.go
.PHONY: format
test:
go test -v ./...
.PHONY: format test

194
README.md
View File

@ -27,18 +27,26 @@ A minimal forward authentication service that provides OAuth/SSO login and authe
- [Concepts](#concepts)
- [Forwarded Headers](#forwarded-headers)
- [User Restriction](#user-restriction)
- [Applying Authentication](#applying-authentication)
- [Global Authentication](#global-authentication)
- [Selective Ingress Authentication in Kubernetes](#selective-ingress-authentication-in-kubernetes)
- [Selective Container Authentication in Swarm](#selective-container-authentication-in-swarm)
- [Rules Based Authentication](#rules-based-authentication)
- [Operation Modes](#operation-modes)
- [Overlay Mode](#overlay-mode)
- [Auth Host Mode](#auth-host-mode)
- [Logging Out](#logging-out)
- [Copyright](#copyright)
- [License](#license)
## Releases
We recommend using the `2` tag on docker hub.
We recommend using the `2` tag on docker hub (`thomseddon/traefik-forward-auth:2`).
You can also use the latest incremental releases found on [docker hub](https://hub.docker.com/r/thomseddon/traefik-forward-auth/tags) and [github](https://github.com/thomseddon/traefik-forward-auth/releases).
ARM releases are also available on docker hub, just append `-arm` or `-arm64` to your desired released (e.g. `2-arm` or `2.1-arm64`).
#### Upgrade Guide
v2 was released in June 2019, whilst this is fully backwards compatible, a number of configuration options were modified, please see the [upgrade guide](https://github.com/thomseddon/traefik-forward-auth/wiki/v2-Upgrade-Guide) to prevent warnings on startup and ensure you are using the current configuration.
@ -56,11 +64,11 @@ version: '3'
services:
traefik:
image: traefik:1.7
image: traefik:v2.2
command: --providers.docker
ports:
- "8085:80"
volumes:
- ./traefik.toml:/traefik.toml
- /var/run/docker.sock:/var/run/docker.sock
traefik-forward-auth:
@ -70,37 +78,28 @@ services:
- PROVIDERS_GOOGLE_CLIENT_SECRET=your-client-secret
- SECRET=something-random
- INSECURE_COOKIE=true # Example assumes no https, do not use in production
labels:
- "traefik.http.middlewares.traefik-forward-auth.forwardauth.address=http://traefik-forward-auth:4181"
- "traefik.http.middlewares.traefik-forward-auth.forwardauth.authResponseHeaders=X-Forwarded-User"
- "traefik.http.services.traefik-forward-auth.loadbalancer.server.port=4181"
whoami:
image: emilevauge/whoami:latest
image: containous/whoami
labels:
- "traefik.frontend.rule=Host:whoami.mycompany.com"
```
traefik.toml:
```toml
[entryPoints]
[entryPoints.http]
address = ":80"
[entryPoints.http.auth.forward]
address = "http://traefik-forward-auth:4181"
authResponseHeaders = ["X-Forwarded-User"]
[docker]
endpoint = "unix:///var/run/docker.sock"
network = "traefik"
- "traefik.http.routers.whoami.rule=Host(`whoami.mycompany.com`)"
- "traefik.http.routers.whoami.middlewares=traefik-forward-auth"
```
#### Advanced:
Please see the examples directory for a more complete [docker-compose.yml](https://github.com/thomseddon/traefik-forward-auth/blob/master/examples/docker-compose.yml) and full [traefik.toml](https://github.com/thomseddon/traefik-forward-auth/blob/master/examples/traefik.toml).
Please see the examples directory for a more complete [docker-compose.yml](https://github.com/thomseddon/traefik-forward-auth/blob/master/examples/traefik-v2/swarm/docker-compose.yml) or [kubernetes/simple-separate-pod](https://github.com/thomseddon/traefik-forward-auth/blob/masterexamples/traefik-v2/kubernetes/simple-separate-pod/).
Also in the examples directory is [docker-compose-auth-host.yml](https://github.com/thomseddon/traefik-forward-auth/blob/master/examples/docker-compose-auth-host.yml) which shows how to configure a central auth host, along with some other options.
Also in the examples directory is [docker-compose-auth-host.yml](https://github.com/thomseddon/traefik-forward-auth/blob/master/examples/traefik-v2/swarm/docker-compose-auth-host.yml) and [kubernetes/advanced-separate-pod](https://github.com/thomseddon/traefik-forward-auth/blob/master/examples/traefik-v2/kubernetes/advanced-separate-pod/) which shows how to configure a central auth host, along with some other options.
#### Provider Setup
Below are some general notes on provider setup, specific instructions and examples for a number of providers can be found on the [Provider Setup](https://github.com/thomseddon/traefik-forward-auth/wiki/Provider-Setup) wiki page.
##### Google
Head to https://console.developers.google.com and make sure you've switched to the correct email account.
@ -117,6 +116,25 @@ Any provider that supports OpenID Connect 1.0 can be configured via the OIDC con
You must set the `providers.oidc.issuer-url`, `providers.oidc.client-id` and `providers.oidc.client-secret` config options.
Please see the [Provider Setup](https://github.com/thomseddon/traefik-forward-auth/wiki/Provider-Setup) wiki page for examples.
##### Generic OAuth2
For providers that don't support OpenID Connect, we also have the Generic OAuth2 provider where you can statically configure the OAuth2 and "user" endpoints.
You must set:
- `providers.generic-oauth.auth-url` - URL the client should be sent to authenticate the authenticate
- `providers.generic-oauth.token-url` - URL the service should call to exchange an auth code for an access token
- `providers.generic-oauth.user-url` - URL used to retrieve user info (service makes a GET request)
- `providers.generic-oauth.client-id` - Client ID
- `providers.generic-oauth.client-secret` - Client Secret
You can also set:
- `providers.generic-oauth.scope`- Any scopes that should be included in the request (default: profile, email)
- `providers.generic-oauth.token-style` - How token is presented when querying the User URL. Can be `header` or `query`, defaults to `header`. With `header` the token is provided in an Authorization header, with query the token is provided in the `access_token` query string value.
Please see the [Provider Setup](https://github.com/thomseddon/traefik-forward-auth/wiki/Provider-Setup) wiki page for examples.
## Configuration
### Overview
@ -137,9 +155,10 @@ Application Options:
--cookie-name= Cookie Name (default: _forward_auth) [$COOKIE_NAME]
--csrf-cookie-name= CSRF Cookie Name (default: _forward_auth_csrf) [$CSRF_COOKIE_NAME]
--default-action=[auth|allow] Default action (default: auth) [$DEFAULT_ACTION]
--default-provider=[google|oidc] Default provider (default: google) [$DEFAULT_PROVIDER]
--default-provider=[google|oidc|generic-oauth] Default provider (default: google) [$DEFAULT_PROVIDER]
--domain= Only allow given email domains, can be set multiple times [$DOMAIN]
--lifetime= Lifetime in seconds (default: 43200) [$LIFETIME]
--logout-redirect= URL to redirect to following logout [$LOGOUT_REDIRECT]
--url-path= Callback URL Path (default: /_oauth) [$URL_PATH]
--secret= Secret used for signing (required) [$SECRET]
--whitelist= Only allow given email addresses, can be set multiple times [$WHITELIST]
@ -154,6 +173,18 @@ OIDC Provider:
--providers.oidc.issuer-url= Issuer URL [$PROVIDERS_OIDC_ISSUER_URL]
--providers.oidc.client-id= Client ID [$PROVIDERS_OIDC_CLIENT_ID]
--providers.oidc.client-secret= Client Secret [$PROVIDERS_OIDC_CLIENT_SECRET]
--providers.oidc.resource= Optional resource indicator [$PROVIDERS_OIDC_RESOURCE]
Generic OAuth2 Provider:
--providers.generic-oauth.auth-url= Auth/Login URL [$PROVIDERS_GENERIC_OAUTH_AUTH_URL]
--providers.generic-oauth.token-url= Token URL [$PROVIDERS_GENERIC_OAUTH_TOKEN_URL]
--providers.generic-oauth.user-url= URL used to retrieve user info [$PROVIDERS_GENERIC_OAUTH_USER_URL]
--providers.generic-oauth.client-id= Client ID [$PROVIDERS_GENERIC_OAUTH_CLIENT_ID]
--providers.generic-oauth.client-secret= Client Secret [$PROVIDERS_GENERIC_OAUTH_CLIENT_SECRET]
--providers.generic-oauth.scope= Scopes (default: profile, email) [$PROVIDERS_GENERIC_OAUTH_SCOPE]
--providers.generic-oauth.token-style=[header|query] How token is presented when querying the User URL (default: header)
[$PROVIDERS_GENERIC_OAUTH_TOKEN_STYLE]
--providers.generic-oauth.resource= Optional resource indicator [$PROVIDERS_GENERIC_OAUTH_RESOURCE]
Help Options:
-h, --help Show this help message
@ -247,6 +278,20 @@ All options can be supplied in any of the following ways, in the following prece
Default: `43200` (12 hours)
- `logout-redirect`
When set, users will be redirected to this URL following logout.
- `match-whitelist-or-domain`
When enabled, users will be permitted if they match *either* the `whitelist` or `domain` parameters.
This will be enabled by default in v3, but is disabled by default in v2 to maintain backwards compatibility.
Default: `false`
For more details, please also read [User Restriction](#user-restriction) in the concepts section.
- `url-path`
Customise the path that this service uses to handle the callback following authentication.
@ -305,6 +350,8 @@ All options can be supplied in any of the following ways, in the following prece
rule.oidc.rule = PathPrefix(`/github`)
```
Note: It is possible to break your redirect flow with rules, please be careful not to create an `allow` rule that matches your redirect_uri unless you know what you're doing. This limitation is being tracked in in #101 and the behaviour will change in future releases.
## Concepts
### User Restriction
@ -314,11 +361,94 @@ You can restrict who can login with the following parameters:
* `domain` - Use this to limit logins to a specific domain, e.g. test.com only
* `whitelist` - Use this to only allow specific users to login e.g. thom@test.com only
Note, if you pass `whitelist` then only this is checked and `domain` is effectively ignored.
Note, if you pass both `whitelist` and `domain`, then the default behaviour is for only `whitelist` to be used and `domain` will be effectively ignored. You can allow users matching *either* `whitelist` or `domain` by passing the `match-whitelist-or-domain` parameter (this will be the default behaviour in v3).
### Forwarded Headers
The authenticated user is set in the `X-Forwarded-User` header, to pass this on add this to the `authResponseHeaders` config option in traefik, as shown [here](https://github.com/thomseddon/traefik-forward-auth/blob/master/examples/docker-compose-dev.yml).
The authenticated user is set in the `X-Forwarded-User` header, to pass this on add this to the `authResponseHeaders` config option in traefik, as shown below in the [Applying Authentication](#applying-authentication) section.
### Applying Authentication
Authentication can be applied in a variety of ways, either globally across all requests, or selectively to specific containers/ingresses.
#### Global Authentication
This can be achieved by enabling forward authentication for an entire entrypoint, for example, with http only:
```ini
--entryPoints.http.address=:80
--entrypoints.http.http.middlewares=traefik-forward-auth # "default-traefik-forward-auth" on kubernetes
```
Or https:
```ini
--entryPoints.http.address=:80
--entryPoints.http.http.redirections.entryPoint.to=https
--entryPoints.http.http.redirections.entryPoint.scheme=https
--entryPoints.https.address=:443
--entrypoints.https.http.middlewares=traefik-forward-auth # "default-traefik-forward-auth" on kubernetes
```
Note: Traefik prepends the namespace to the name of middleware defined via a kubernetes resource. This is handled automatically when referencing the middleware from another resource in the same namespace (so the namespace does not need to be prepended when referenced). However the full name, including the namespace, must be used when referenced from static configuration (e.g. command arguments or config file), hence you must prepend the namespace to your traefik-forward-auth middleware reference, as shown in the comments above (e.g. `default-traefik-forward-auth` if your middleware is named `traefik-forward-auth` and is defined in the `default` namespace).
#### Selective Ingress Authentication in Kubernetes
If you choose not to enable forward authentication for a specific entrypoint, you can apply the middleware to selected ingressroutes:
```yaml
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
name: whoami
labels:
app: whoami
spec:
entryPoints:
- http
routes:
- match: Host(`whoami.example.com`)
kind: Rule
services:
- name: whoami
port: 80
middlewares:
- name: traefik-forward-auth
```
Note: If using auth host mode, you must apply the middleware to your auth host ingress.
See the examples directory for more examples.
#### Selective Container Authentication in Swarm
You can apply labels to selected containers:
```yaml
whoami:
image: containous/whoami
labels:
- "traefik.http.routers.whoami.rule=Host(`whoami.example.com`)"
- "traefik.http.routers.whoami.middlewares=traefik-forward-auth"
```
Note: If using auth host mode, you must apply the middleware to the traefik-forward-auth container.
See the examples directory for more examples.
#### Rules Based Authentication
You can also leverage the `rules` config to selectively apply authentication via traefik-forward-auth. For example if you enabled global authentication by enabling forward authentication for an entire entrypoint, you can still exclude some patterns from requiring authentication:
```ini
# Allow requests to 'dash.example.com'
rule.1.action = allow
rule.1.rule = Host(`dash.example.com`)
# Allow requests to `app.example.com/public`
rule.two.action = allow
rule.two.rule = Host(`app.example.com`) && Path(`/public`)
```
### Operation Modes
@ -339,7 +469,7 @@ As the hostname in the `redirect_uri` is dynamically generated based on the orig
#### Auth Host Mode
This is an optional mode of operation that is useful when dealing with a large number of subdomains, it is activated by using the `auth-host` config option (see [this example docker-compose.yml](https://github.com/thomseddon/traefik-forward-auth/blob/master/examples/docker-compose-auth-host.yml)).
This is an optional mode of operation that is useful when dealing with a large number of subdomains, it is activated by using the `auth-host` config option (see [this example docker-compose.yml](https://github.com/thomseddon/traefik-forward-auth/blob/master/examples/traefik-v2/swarm/docker-compose-auth-host.yml) or [this kubernetes example](https://github.com/thomseddon/traefik-forward-auth/tree/master/examples/traefik-v2/kubernetes/advanced-separate-pod)).
For example, if you have a few applications: `app1.test.com`, `app2.test.com`, `appN.test.com`, adding every domain to Google's console can become laborious.
To utilise an auth host, permit domain level cookies by setting the cookie domain to `test.com` then set the `auth-host` to: `auth.test.com`.
@ -360,7 +490,15 @@ Two criteria must be met for an `auth-host` to be used:
1. Request matches given `cookie-domain`
2. `auth-host` is also subdomain of same `cookie-domain`
Please note: For Auth Host mode to work, you must ensure that requests to your auth-host are routed to the traefik-forward-auth container, as demonstrated with the service labels in the [docker-compose-auth.yml](https://github.com/thomseddon/traefik-forward-auth/blob/master/examples/docker-compose-auth-host.yml) example.
Please note: For Auth Host mode to work, you must ensure that requests to your auth-host are routed to the traefik-forward-auth container, as demonstrated with the service labels in the [docker-compose-auth.yml](https://github.com/thomseddon/traefik-forward-auth/blob/master/examples/traefik-v2/swarm/docker-compose-auth-host.yml) example and the [ingressroute resource](https://github.com/thomseddon/traefik-forward-auth/blob/master/examples/traefik-v2/kubernetes/advanced-separate-pod/traefik-forward-auth/ingress.yaml) in a kubernetes example.
### Logging Out
The service provides an endpoint to clear a users session and "log them out". The path is created by appending `/logout` to your configured `path` and so with the default settings it will be: `/_oauth/logout`.
You can use the `logout-redirect` config option to redirect users to another URL following logout (note: the user will not have a valid auth cookie after being logged out).
Note: This only clears the auth cookie from the users browser and as this service is stateless, it does not invalidate the cookie against future use. So if the cookie was recorded, for example, it could continue to be used for the duration of the cookie lifetime.
## Copyright

View File

@ -24,7 +24,7 @@ func main() {
http.HandleFunc("/", server.RootHandler)
// Start
log.Debugf("Starting with options: %s", config)
log.WithField("config", config).Debug("Starting with config")
log.Info("Listening on :4181")
log.Info(http.ListenAndServe(":4181", nil))
}

View File

@ -1,48 +0,0 @@
version: '3'
services:
traefik:
image: traefik
command: -c /traefik.toml
# command: -c /traefik.toml --logLevel=DEBUG
ports:
- "8085:80"
- "8086:8080"
networks:
- traefik
volumes:
- ./traefik.toml:/traefik.toml
- /var/run/docker.sock:/var/run/docker.sock
whoami1:
image: emilevauge/whoami
networks:
- traefik
labels:
- "traefik.backend=whoami1"
- "traefik.enable=true"
- "traefik.frontend.rule=Host:whoami.localhost.com"
whoami2:
image: emilevauge/whoami
networks:
- traefik
labels:
- "traefik.backend=whoami2"
- "traefik.enable=true"
- "traefik.frontend.rule=Host:whoami.localhost.org"
traefik-forward-auth:
build: ../
environment:
- PROVIDERS_GOOGLE_CLIENT_ID=your-client-id
- PROVIDERS_GOOGLE_CLIENT_SECRET=your-client-secret
- SECRET=something-random
- INSECURE_COOKIE=true
- COOKIE_DOMAIN=localhost.com
- AUTH_HOST=auth.localhost.com
networks:
- traefik
networks:
traefik:

View File

@ -0,0 +1,10 @@
# Kubernetes
These examples show how to deploy traefik-forward-auth alongside traefik v1.7.
The "seperate pod" examples show traefik-forward-auth in it's own pod and leave the deployment of traefik as an exercise for the user (e.g. if using helm).
The "single pod" examples show traefik and traefik-forward-auth in a single pod.
Please see the README's in each example for more details.

View File

@ -0,0 +1,16 @@
# Kubernetes - Advanced Separate Pod Example
This is an advanced example of how to deploy traefik-forward-auth in it's own pod. This example is a good starting point for those who already have traefik deployed (e.g. using helm).
This example uses [Selective Authentication](https://github.com/thomseddon/traefik-forward-auth/blob/master/README.md#selective-ingress-authentication-in-kubernetes) to selectively apply forward authentication to each selective ingress, a simple example "whoami" application (deployment, service and ingress) is included for completeness.
This example leverages kustomise to define Secrets and ConfigMaps, example deployment:
```
# Deploy traefik-forward-auth
kubectl apply -k traefik-forward-auth
# Deploy example whoami app
kubectl apply -k whoami
```

View File

@ -0,0 +1,3 @@
bases:
- traefik-forward-auth
- whoami

View File

@ -0,0 +1,68 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: traefik-forward-auth
labels:
app: traefik-forward-auth
spec:
replicas: 1
selector:
matchLabels:
app: traefik-forward-auth
strategy:
type: Recreate
template:
metadata:
labels:
app: traefik-forward-auth
spec:
terminationGracePeriodSeconds: 60
containers:
- image: thomseddon/traefik-forward-auth:2
name: traefik-forward-auth
ports:
- containerPort: 4181
protocol: TCP
env:
- name: CONFIG
value: "/config"
- name: DOMAIN
value: "example.com"
# INSECURE_COOKIE is required unless using https entrypoint
- name: INSECURE_COOKIE
value: "true"
# Remove COOKIE_DOMAIN if not using auth host mode
- name: COOKIE_DOMAIN
value: "example.com"
# Remove AUTH_HOST if not using auth host mode
- name: AUTH_HOST
value: "auth.example.com"
- name: LOG_LEVEL
value: "info"
- name: PROVIDERS_GOOGLE_CLIENT_ID
valueFrom:
secretKeyRef:
name: traefik-forward-auth-secrets
key: google-client-id
- name: PROVIDERS_GOOGLE_CLIENT_SECRET
valueFrom:
secretKeyRef:
name: traefik-forward-auth-secrets
key: google-client-secret
- name: SECRET
valueFrom:
secretKeyRef:
name: traefik-forward-auth-secrets
key: secret
volumeMounts:
- name: configs
mountPath: /config
subPath: traefik-forward-auth.ini
volumes:
- name: configs
configMap:
name: configs
- name: traefik-forward-auth-secrets
secret:
secretName: secrets

View File

@ -0,0 +1,22 @@
#
# NOTE: This is only needed if you are using auth-host mode
#
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: traefik-forward-auth
labels:
app: traefik-forward-auth
annotations:
kubernetes.io/ingress.class: traefik
ingress.kubernetes.io/auth-type: forward
ingress.kubernetes.io/auth-url: http://traefik-forward-auth:4181
ingress.kubernetes.io/auth-response-headers: X-Forwarded-User
spec:
rules:
- host: auth.example.com
http:
paths:
- backend:
serviceName: traefik-forward-auth
servicePort: auth-http

View File

@ -0,0 +1,22 @@
commonLabels:
app: traefik-forward-auth
resources:
- deployment.yaml
- service.yaml
- ingress.yaml # Only needed for auth-host mode
#
# Configs
#
configMapGenerator:
- name: configs
files:
- traefik-forward-auth.ini
#
# Secrets
#
secretGenerator:
- name: traefik-forward-auth-secrets
env: traefik-forward-auth.env

View File

@ -0,0 +1,14 @@
apiVersion: v1
kind: Service
metadata:
name: traefik-forward-auth
labels:
app: traefik-forward-auth
spec:
type: ClusterIP
selector:
app: traefik-forward-auth
ports:
- name: auth-http
port: 4181
targetPort: 4181

View File

@ -0,0 +1,3 @@
google-client-id=client-id
google-client-secret=client-secret
secret=something-random

View File

@ -0,0 +1,8 @@
rule.example_public.action=allow
rule.example_public.rule=Host("stats.example.com") && PathPrefix("/api/public")
rule.example_api.action=allow
rule.example_api.rule=Host("api.example.com") && Headers("X-API-Authorization", "a-long-api-key")
rule.example_api_query.action=allow
rule.example_api_query.rule=Host("api.example.com") && && Query("api_key=a-long-api-key")

View File

@ -0,0 +1,19 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: whoami
labels:
app: whoami
spec:
replicas: 1
selector:
matchLabels:
app: whoami
template:
metadata:
labels:
app: whoami
spec:
containers:
- image: containous/whoami
name: whoami

View File

@ -0,0 +1,19 @@
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: whoami
labels:
app: whoami
annotations:
kubernetes.io/ingress.class: traefik
ingress.kubernetes.io/auth-type: forward
ingress.kubernetes.io/auth-url: http://traefik-forward-auth:4181
ingress.kubernetes.io/auth-response-headers: X-Forwarded-User
spec:
rules:
- host: whoami.example.com
http:
paths:
- backend:
serviceName: whoami
servicePort: http

View File

@ -0,0 +1,7 @@
commonLabels:
app: whoami
resources:
- deployment.yaml
- service.yaml
- ingress.yaml

View File

@ -0,0 +1,14 @@
apiVersion: v1
kind: Service
metadata:
name: whoami
labels:
app: whoami
spec:
type: ClusterIP
ports:
- name: http
port: 80
selector:
app: whoami

View File

@ -0,0 +1,18 @@
# Kubernetes - Advanced Single Pod Example
This is an advanced example of how to deploy traefik and traefik-forward-auth in a single pod. This example is a good starting point for those who already have a manually defined traefik config (e.g. not using helm).
This example uses [Global Authentication](https://github.com/thomseddon/traefik-forward-auth/blob/master/README.md#global-authentication) to apply authentication for the entire `https` entrypoint.
This example also includes SSL via traefik acme/lesencrypt, auth host mode, exposes the traefik dashboard and leverages kustomise. No special config if required for your applications, but a simple example "whoami" application (deployment, service and ingress) is included for completeness.
Example deployment:
```
# Deploy traefik+traefik-forward-auth
kubectl apply -k traefik
# Deploy whoami app
kubectl apply -k whoami
```

View File

@ -0,0 +1,3 @@
bases:
- traefik
- whoami

View File

@ -0,0 +1,8 @@
rule.example_public.action=allow
rule.example_public.rule=Host("stats.example.com") && PathPrefix("/api/public")
rule.example_api.action=allow
rule.example_api.rule=Host("api.example.com") && Headers("X-API-Authorization", "a-long-api-key")
rule.example_api_query.action=allow
rule.example_api_query.rule=Host("api.example.com") && && Query("api_key=a-long-api-key")

View File

@ -0,0 +1,169 @@
################################################################
# Global configuration
################################################################
# Enable debug mode
#
# Optional
# Default: false
#
# debug = true
# Log level
#
# Optional
# Default: "ERROR"
#
logLevel = "INFO"
# Entrypoints to be used by frontends that do not specify any entrypoint.
# Each frontend can specify its own entrypoints.
#
# Optional
# Default: ["http"]
#
defaultEntryPoints = ["http", "https"]
# If set to true invalid SSL certificates are accepted for backends.
# This disables detection of man-in-the-middle attacks so should only be used on secure backend networks.
#
# Optional
# Default: false
#
insecureSkipVerify = true
################################################################
# Entrypoints configuration
################################################################
# Entrypoints definition
#
# Optional
# Default:
[entryPoints]
[entryPoints.http]
address = ":80"
compress = true
[entryPoints.http.redirect]
entryPoint = "https"
[entryPoints.https]
address = ":443"
compress = true
[entryPoints.https.tls]
[entryPoints.https.auth.forward]
address = "http://127.0.0.1:4181"
authResponseHeaders = ["X-Forwarded-User"]
[entryPoints.traefik]
address = ":8080"
################################################################
# Traefik logs configuration
################################################################
# Traefik logs
# Enabled by default and log to stdout
#
# Optional
#
[traefikLog]
format = "json"
# Sets the filepath for the traefik log. If not specified, stdout will be used.
# Intermediate directories are created if necessary.
#
# Optional
# Default: os.Stdout
#
# filePath = "log/traefik.log"
# Format is either "json" or "common".
#
# Optional
# Default: "common"
#
# format = "common"
################################################################
# Access logs configuration
################################################################
# Enable access logs
# By default it will write to stdout and produce logs in the textual
# Common Log Format (CLF), extended with additional fields.
#
# Optional
#
# [accessLog]
# Sets the file path for the access log. If not specified, stdout will be used.
# Intermediate directories are created if necessary.
#
# Optional
# Default: os.Stdout
#
# filePath = "/path/to/log/log.txt"
# Format is either "json" or "common".
#
# Optional
# Default: "common"
#
# format = "common"
################################################################
# API and dashboard configuration
################################################################
# Enable API and dashboard
[api]
# Name of the related entry point
#
# Optional
# Default: "traefik"
#
# entryPoint = "traefik"
# Enabled Dashboard
#
# Optional
# Default: true
#
# dashboard = false
################################################################
# Ping configuration
################################################################
# Enable ping
[ping]
# Name of the related entry point
#
# Optional
# Default: "traefik"
#
# entryPoint = "traefik"
################################################################
# Docker configuration backend
################################################################
# Enable Kubernetes configuration backend
[kubernetes]
[acme]
KeyType = "RSA4096"
email = "you@example.com"
storage = "/acme/acme.json"
entryPoint = "https"
onHostRule = true
acmeLogging = true
[acme.httpChallenge]
entryPoint = "http"

View File

@ -0,0 +1,94 @@
#
# Traefik + Traefik Forward Auth Deployment
#
apiVersion: apps/v1
kind: Deployment
metadata:
name: traefik
labels:
app: traefik
spec:
replicas: 1
selector:
matchLabels:
app: traefik
strategy:
type: Recreate
template:
metadata:
labels:
app: traefik
spec:
serviceAccountName: traefik
terminationGracePeriodSeconds: 60
containers:
- image: traefik:1.7.12
name: traefik
args:
- --configfile=/config/traefik.toml
ports:
- name: http
containerPort: 80
protocol: TCP
- name: https
containerPort: 443
protocol: TCP
- name: dash
containerPort: 8080
protocol: TCP
volumeMounts:
- mountPath: /config
name: configs
- mountPath: /acme
name: acme
- image: thomseddon/traefik-forward-auth:2
name: traefik-forward-auth
ports:
- containerPort: 4181
protocol: TCP
env:
- name: CONFIG
value: "/config"
- name: DOMAIN
value: "example.com"
# INSECURE_COOKIE is required if not using a https entrypoint
# - name: INSECURE_COOKIE
# value: "true"
# Remove COOKIE_DOMAIN if not using auth host mode
- name: COOKIE_DOMAIN
value: "example.com"
- name: AUTH_HOST
value: "auth.example.com"
- name: LOG_LEVEL
value: "info"
- name: PROVIDERS_GOOGLE_CLIENT_ID
valueFrom:
secretKeyRef:
name: traefik-forward-auth-secrets
key: google-client-id
- name: PROVIDERS_GOOGLE_CLIENT_SECRET
valueFrom:
secretKeyRef:
name: traefik-forward-auth-secrets
key: google-client-secret
- name: SECRET
valueFrom:
secretKeyRef:
name: traefik-forward-auth-secrets
key: secret
volumeMounts:
- name: configs
mountPath: /config
subPath: traefik-forward-auth.ini
volumes:
- name: configs
configMap:
name: configs
- name: traefik-forward-auth-secrets
secret:
secretName: secrets
- name: acme
persistentVolumeClaim:
claimName: traefik-acme

View File

@ -0,0 +1,36 @@
#
# Auth Ingress
#
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: traefik-forward-auth
labels:
app: traefik
spec:
rules:
- host: auth.example.com
http:
paths:
- backend:
serviceName: traefik-forward-auth
servicePort: auth-http
---
#
# Dash Ingress
#
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: traefik-dashboard
labels:
app: traefik
spec:
rules:
- host: traefik.example.com
http:
paths:
- backend:
serviceName: traefik-dashboard
servicePort: dashboard-http

View File

@ -0,0 +1,28 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: default
commonLabels:
app: traefik
resources:
- deployment.yaml
- service.yaml
- ingress.yaml
- pvc.yaml
- rbac.yaml
#
# Configs
#
configMapGenerator:
- name: configs
files:
- configs/traefik.toml
- configs/traefik-forward-auth.ini
#
# Secrets
#
secretGenerator:
- name: traefik-forward-auth-secrets
env: secrets/traefik-forward-auth.env

View File

@ -0,0 +1,17 @@
# Source: traefik/templates/acme-pvc.yaml
#
# PVC
#
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: traefik-acme
labels:
app: traefik
spec:
accessModes:
- "ReadWriteOnce"
resources:
requests:
storage: "1Gi"
storageClassName: "local-traefik-acme"

View File

@ -0,0 +1,52 @@
#
# RBAC
# Source: traefik/templates/rbac.yaml
#
kind: ServiceAccount
apiVersion: v1
metadata:
name: traefik
---
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: traefik
rules:
- apiGroups:
- ""
resources:
- pods
- services
- endpoints
- secrets
verbs:
- get
- list
- watch
- apiGroups:
- extensions
resources:
- ingresses
verbs:
- get
- list
- watch
- apiGroups:
- extensions
resources:
- ingresses/status
verbs:
- update
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: traefik
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: traefik
subjects:
- kind: ServiceAccount
name: traefik
namespace: kube-system

View File

@ -0,0 +1,3 @@
google-client-id=client-id
google-client-secret=client-secret
secret=something-random

View File

@ -0,0 +1,58 @@
#
# Traefik Service
#
apiVersion: v1
kind: Service
metadata:
name: traefik
labels:
app: traefik
spec:
# Use NodePort if required
type: LoadBalancer
selector:
app: traefik
ports:
- name: http
port: 80
targetPort: 80
- name: https
port: 443
targetPort: 443
---
#
# Auth Service
#
apiVersion: v1
kind: Service
metadata:
name: traefik-forward-auth
labels:
app: traefik
spec:
type: ClusterIP
selector:
app: traefik
ports:
- name: auth-http
port: 4181
targetPort: 4181
---
#
# Dash Service
#
apiVersion: v1
kind: Service
metadata:
name: traefik-dashboard
labels:
app: traefik
spec:
type: ClusterIP
selector:
app: traefik
ports:
- name: dashboard-http
port: 8080
targetPort: 8080

View File

@ -0,0 +1,19 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: whoami
labels:
app: whoami
spec:
replicas: 1
selector:
matchLabels:
app: whoami
template:
metadata:
labels:
app: whoami
spec:
containers:
- image: containous/whoami
name: whoami

View File

@ -0,0 +1,16 @@
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: whoami
labels:
app: whoami
annotations:
kubernetes.io/ingress.class: traefik
spec:
rules:
- host: whoami.example.com
http:
paths:
- backend:
serviceName: whoami
servicePort: http

View File

@ -0,0 +1,7 @@
commonLabels:
app: whoami
resources:
- deployment.yaml
- service.yaml
- ingress.yaml

View File

@ -0,0 +1,14 @@
apiVersion: v1
kind: Service
metadata:
name: whoami
labels:
app: whoami
spec:
type: ClusterIP
ports:
- name: http
port: 80
selector:
app: whoami

View File

@ -0,0 +1,43 @@
# Kubernetes - Simple Separate Pod Example
This is a simple example of how to deploy traefik-forward-auth in it's own pod with minimal configuration. This example is a good starting point for those who already have traefik deployed (e.g. using helm).
This example uses annotations to apply authentication to selected ingresses (see `k8s-app.yml`). This means ingresses will not be protected by default, only those with these annotations will require forward authentication. For example:
```
#
# Ingress
#
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: whoami
labels:
app: whoami
annotations:
kubernetes.io/ingress.class: traefik
ingress.kubernetes.io/auth-type: forward
ingress.kubernetes.io/auth-url: http://traefik-forward-auth:4181
ingress.kubernetes.io/auth-response-headers: X-Forwarded-User
spec:
rules:
- host: whoami.example.com
http:
paths:
- backend:
serviceName: whoami
servicePort: http
```
Example deployment:
```
# Deploy traefik-forward-auth
kubectl apply -f k8s-traefik-forward-auth.yml
# Deploy example whoami app
kubectl apply -f k8s-app.yml
```
Please see the advanced examples for more details.

View File

@ -0,0 +1,62 @@
#
# Example Application Deployment
#
apiVersion: apps/v1
kind: Deployment
metadata:
name: whoami
labels:
app: whoami
spec:
replicas: 1
selector:
matchLabels:
app: whoami
template:
metadata:
labels:
app: whoami
spec:
containers:
- name: whoami
image: containous/whoami
---
#
# Service
#
apiVersion: v1
kind: Service
metadata:
name: whoami
labels:
app: whoami
spec:
ports:
- name: http
port: 80
selector:
app: whoami
---
#
# Ingress
#
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: whoami
labels:
app: whoami
annotations:
kubernetes.io/ingress.class: traefik
ingress.kubernetes.io/auth-type: forward
ingress.kubernetes.io/auth-url: http://traefik-forward-auth:4181
ingress.kubernetes.io/auth-response-headers: X-Forwarded-User
spec:
rules:
- host: whoami.example.com
http:
paths:
- backend:
serviceName: whoami
servicePort: http

View File

@ -0,0 +1,90 @@
#
# Traefik Forward Auth Deployment
#
apiVersion: apps/v1
kind: Deployment
metadata:
name: traefik-forward-auth
labels:
app: traefik-forward-auth
spec:
replicas: 1
selector:
matchLabels:
app: traefik-forward-auth
strategy:
type: Recreate
template:
metadata:
labels:
app: traefik-forward-auth
spec:
terminationGracePeriodSeconds: 60
containers:
- image: thomseddon/traefik-forward-auth:2
name: traefik-forward-auth
ports:
- containerPort: 4181
protocol: TCP
env:
- name: DOMAIN
value: "example.com"
# INSECURE_COOKIE is required unless using https entrypoint
- name: INSECURE_COOKIE
value: "true"
- name: PROVIDERS_GOOGLE_CLIENT_ID
valueFrom:
secretKeyRef:
name: traefik-forward-auth-secrets
key: traefik-forward-auth-google-client-id
- name: PROVIDERS_GOOGLE_CLIENT_SECRET
valueFrom:
secretKeyRef:
name: traefik-forward-auth-secrets
key: traefik-forward-auth-google-client-secret
- name: SECRET
valueFrom:
secretKeyRef:
name: traefik-forward-auth-secrets
key: traefik-forward-auth-secret
---
#
# Auth Service
#
apiVersion: v1
kind: Service
metadata:
name: traefik-forward-auth
labels:
app: traefik-forward-auth
spec:
type: ClusterIP
selector:
app: traefik-forward-auth
ports:
- name: auth-http
port: 4181
targetPort: 4181
---
#
# Secrets
#
# Kubernetes requires secret values to be converted to base64 when defined
# explicitly like this. (use `echo -n 'secret-value' | base64`)
#
# These are here for completeness, in reality you may define these elsewhere,
# for example using kustomize (shown in advanced examples)
#
apiVersion: v1
kind: Secret
metadata:
name: traefik-forward-auth-secrets
labels:
app: traefik-forward-auth
type: Opaque
data:
traefik-forward-auth-google-client-id: base64-client-id
traefik-forward-auth-google-client-secret: base64-client-secret
traefik-forward-auth-secret: base64-something-random

View File

@ -14,7 +14,7 @@ services:
- /var/run/docker.sock:/var/run/docker.sock
whoami1:
image: emilevauge/whoami
image: containous/whoami
networks:
- traefik
labels:
@ -23,7 +23,7 @@ services:
- "traefik.frontend.rule=Host:whoami.yourdomain.com"
traefik-forward-auth:
image: thomseddon/traefik-forward-auth
image: thomseddon/traefik-forward-auth:2
environment:
- PROVIDERS_GOOGLE_CLIENT_ID=your-client-id
- PROVIDERS_GOOGLE_CLIENT_SECRET=your-client-secret

View File

@ -14,7 +14,7 @@ services:
- /var/run/docker.sock:/var/run/docker.sock
whoami1:
image: emilevauge/whoami
image: containous/whoami
networks:
- traefik
labels:
@ -23,7 +23,7 @@ services:
- "traefik.frontend.rule=Host:whoami.localhost.com"
traefik-forward-auth:
build: ../
build: thomseddon/traefik-forward-auth:2
environment:
- DEFAULT_PROVIDER=oidc
- PROVIDERS_OIDC_ISSUER_URL=https://login.microsoftonline.com/{tenant}

View File

@ -14,7 +14,7 @@ services:
- /var/run/docker.sock:/var/run/docker.sock
whoami1:
image: emilevauge/whoami
image: containous/whoami
networks:
- traefik
labels:
@ -23,8 +23,8 @@ services:
- "traefik.frontend.rule=Host:whoami.localhost.com"
traefik-forward-auth:
build: ../
command: ./traefik-forward-auth --rule.1.action=allow --rule.1.rule="Path(`/`)"
build: thomseddon/traefik-forward-auth:2
command: ./traefik-forward-auth --rule.1.action=allow --rule.1.rule="Path(`/public`)"
environment:
- PROVIDERS_GOOGLE_CLIENT_ID=your-client-id
- PROVIDERS_GOOGLE_CLIENT_SECRET=your-client-secret

View File

@ -135,3 +135,4 @@
# Enable Docker configuration backend
[docker]
exposedByDefault = false
network = "traefik"

View File

@ -0,0 +1,39 @@
# Kubernetes - Advanced Separate Pod Example
This is an advanced example of how to deploy traefik-forward-auth in it's own pod. This example is a good starting point for those who already have traefik deployed (e.g. using helm).
This example uses [Selective Authentication](https://github.com/thomseddon/traefik-forward-auth/blob/master/README.md#selective-ingress-authentication-in-kubernetes) to selectively apply forward authentication to each selective ingresses, for example:
```
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
name: whoami
labels:
app: whoami
spec:
entryPoints:
- https
routes:
- match: Host(`whoami.example.com`)
kind: Rule
services:
- name: whoami
port: 80
middlewares:
- name: traefik-forward-auth
tls:
certresolver: default
```
This example also includes SSL via traefik acme/lesencrypt, auth host mode, and leverages kustomise. A simple example "whoami" application (deployment, service and ingress) is included for completeness.
Example deployment:
```
# Deploy traefik-forward-auth
kubectl apply -k traefik-forward-auth
# Deploy example whoami app
kubectl apply -k whoami
```

View File

@ -0,0 +1,3 @@
bases:
- traefik-forward-auth
- whoami

View File

@ -0,0 +1,8 @@
rule.example_public.action=allow
rule.example_public.rule=Host("stats.example.com") && PathPrefix("/api/public")
rule.example_api.action=allow
rule.example_api.rule=Host("api.example.com") && Headers("X-API-Authorization", "a-long-api-key")
rule.example_api_query.action=allow
rule.example_api_query.rule=Host("api.example.com") && && Query("api_key=a-long-api-key")

View File

@ -0,0 +1,71 @@
#
# Traefik Forward Auth Deployment
#
apiVersion: apps/v1
kind: Deployment
metadata:
name: traefik-forward-auth
labels:
app: traefik-forward-auth
spec:
replicas: 1
selector:
matchLabels:
app: traefik-forward-auth
strategy:
type: Recreate
template:
metadata:
labels:
app: traefik-forward-auth
spec:
serviceAccountName: traefik-ingress-controller
terminationGracePeriodSeconds: 60
containers:
- image: thomseddon/traefik-forward-auth:2
name: traefik-forward-auth
ports:
- containerPort: 4181
protocol: TCP
env:
- name: CONFIG
value: "/config"
- name: DOMAIN
value: "example.com"
# INSECURE_COOKIE is required if not using a https entrypoint
# - name: INSECURE_COOKIE
# value: "true"
# Remove COOKIE_DOMAIN if not using auth host mode
- name: COOKIE_DOMAIN
value: "example.com"
- name: AUTH_HOST
value: "auth.example.com"
- name: LOG_LEVEL
value: "info"
- name: PROVIDERS_GOOGLE_CLIENT_ID
valueFrom:
secretKeyRef:
name: traefik-forward-auth-secrets
key: google-client-id
- name: PROVIDERS_GOOGLE_CLIENT_SECRET
valueFrom:
secretKeyRef:
name: traefik-forward-auth-secrets
key: google-client-secret
- name: SECRET
valueFrom:
secretKeyRef:
name: traefik-forward-auth-secrets
key: secret
volumeMounts:
- name: configs
mountPath: /config
subPath: traefik-forward-auth.ini
volumes:
- name: configs
configMap:
name: configs
- name: traefik-forward-auth-secrets
secret:
secretName: traefik-forward-auth-secrets

View File

@ -0,0 +1,22 @@
#
# Auth Ingress
#
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
name: traefik-forward-auth
labels:
app: traefik
spec:
entryPoints:
- https
routes:
- match: Host(`auth.example.com`)
kind: Rule
services:
- name: traefik-forward-auth
port: 4181
middlewares:
- name: traefik-forward-auth
tls:
certresolver: default

View File

@ -0,0 +1,26 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: default
commonLabels:
app: traefik-forward-auth
resources:
- deployment.yaml
- service.yaml
- ingress.yaml
- middleware.yaml
#
# Configs
#
configMapGenerator:
- name: configs
files:
- configs/traefik-forward-auth.ini
#
# Secrets
#
secretGenerator:
- name: traefik-forward-auth-secrets
env: secrets/traefik-forward-auth.env

View File

@ -0,0 +1,9 @@
apiVersion: traefik.containo.us/v1alpha1
kind: Middleware
metadata:
name: traefik-forward-auth
spec:
forwardAuth:
address: http://traefik-forward-auth:4181
authResponseHeaders:
- X-Forwarded-User

View File

@ -0,0 +1,3 @@
google-client-id=client-id
google-client-secret=client-secret
secret=something-random

View File

@ -0,0 +1,17 @@
#
# Auth Service
#
apiVersion: v1
kind: Service
metadata:
name: traefik-forward-auth
labels:
app: traefik
spec:
type: ClusterIP
selector:
app: traefik
ports:
- name: auth-http
port: 4181
targetPort: 4181

View File

@ -0,0 +1,19 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: whoami
labels:
app: whoami
spec:
replicas: 1
selector:
matchLabels:
app: whoami
template:
metadata:
labels:
app: whoami
spec:
containers:
- image: containous/whoami
name: whoami

View File

@ -0,0 +1,19 @@
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
name: whoami
labels:
app: whoami
spec:
entryPoints:
- https
routes:
- match: Host(`whoami.example.com`)
kind: Rule
services:
- name: whoami
port: 80
middlewares:
- name: traefik-forward-auth
tls:
certresolver: default

View File

@ -0,0 +1,7 @@
commonLabels:
app: whoami
resources:
- deployment.yaml
- service.yaml
- ingress.yaml

View File

@ -0,0 +1,14 @@
apiVersion: v1
kind: Service
metadata:
name: whoami
labels:
app: whoami
spec:
type: ClusterIP
ports:
- name: http
port: 80
selector:
app: whoami

View File

@ -0,0 +1,18 @@
# Kubernetes - Advanced Single Pod Example
This is an advanced example of how to deploy traefik and traefik-forward-auth in a single pod. This example is a good starting point for those who already have a manually defined traefik config (e.g. not using helm).
This example uses [Global Authentication](https://github.com/thomseddon/traefik-forward-auth/blob/master/README.md#global-authentication) to apply authentication for the entire `https` entrypoint.
This example also includes SSL via traefik acme/lesencrypt, auth host mode, exposes the traefik dashboard and leverages kustomise. No special config if required for your applications, but a simple example "whoami" application (deployment, service and ingress) is included for completeness.
Example deployment:
```
# Deploy traefik+traefik-forward-auth
kubectl apply -k traefik
# Deploy whoami app
kubectl apply -k whoami
```

View File

@ -0,0 +1,3 @@
bases:
- traefik
- whoami

View File

@ -0,0 +1,8 @@
rule.example_public.action=allow
rule.example_public.rule=Host("stats.example.com") && PathPrefix("/api/public")
rule.example_api.action=allow
rule.example_api.rule=Host("api.example.com") && Headers("X-API-Authorization", "a-long-api-key")
rule.example_api_query.action=allow
rule.example_api_query.rule=Host("api.example.com") && && Query("api_key=a-long-api-key")

View File

@ -0,0 +1,103 @@
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
name: ingressroutes.traefik.containo.us
spec:
group: traefik.containo.us
version: v1alpha1
names:
kind: IngressRoute
plural: ingressroutes
singular: ingressroute
scope: Namespaced
---
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
name: middlewares.traefik.containo.us
spec:
group: traefik.containo.us
version: v1alpha1
names:
kind: Middleware
plural: middlewares
singular: middleware
scope: Namespaced
---
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
name: ingressroutetcps.traefik.containo.us
spec:
group: traefik.containo.us
version: v1alpha1
names:
kind: IngressRouteTCP
plural: ingressroutetcps
singular: ingressroutetcp
scope: Namespaced
---
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
name: ingressrouteudps.traefik.containo.us
spec:
group: traefik.containo.us
version: v1alpha1
names:
kind: IngressRouteUDP
plural: ingressrouteudps
singular: ingressrouteudp
scope: Namespaced
---
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
name: tlsoptions.traefik.containo.us
spec:
group: traefik.containo.us
version: v1alpha1
names:
kind: TLSOption
plural: tlsoptions
singular: tlsoption
scope: Namespaced
---
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
name: tlsstores.traefik.containo.us
spec:
group: traefik.containo.us
version: v1alpha1
names:
kind: TLSStore
plural: tlsstores
singular: tlsstore
scope: Namespaced
---
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
name: traefikservices.traefik.containo.us
spec:
group: traefik.containo.us
version: v1alpha1
names:
kind: TraefikService
plural: traefikservices
singular: traefikservice
scope: Namespaced

View File

@ -0,0 +1,110 @@
#
# Traefik + Traefik Forward Auth Deployment
#
apiVersion: apps/v1
kind: Deployment
metadata:
name: traefik
labels:
app: traefik
spec:
replicas: 1
selector:
matchLabels:
app: traefik
strategy:
type: Recreate
template:
metadata:
labels:
app: traefik
spec:
serviceAccountName: traefik-ingress-controller
terminationGracePeriodSeconds: 60
containers:
- image: traefik:2.2
name: traefik
args:
- --api.dashboard
- --accesslog
- --entryPoints.http.address=:80
- --entryPoints.http.http.redirections.entryPoint.to=https
- --entryPoints.http.http.redirections.entryPoint.scheme=https
- --entryPoints.https.address=:443
# We're using "global authentication", so the middleware is defined here on the entrypoint
# When a kubernetescrd middleware is applied globally it should take the form <kubernetes-namespace>-<middleware>
- --entrypoints.https.http.middlewares=default-traefik-forward-auth
- --providers.kubernetescrd
- --log.level=info
- --log.format=json
- --certificatesresolvers.default.acme.email=foo@you.com
- --certificatesresolvers.default.acme.storage=/acme/acme.json
- --certificatesresolvers.default.acme.storage=/acme/acme.json
- --certificatesresolvers.default.acme.httpchallenge.entrypoint=http
# Please note that this is the staging Let's Encrypt server.
# Once you get things working, you should remove that whole line altogether.
- --certificatesresolvers.default.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory
ports:
- name: http
containerPort: 80
protocol: TCP
- name: https
containerPort: 443
protocol: TCP
- name: dash
containerPort: 8080
protocol: TCP
volumeMounts:
- mountPath: /acme
name: acme
- image: thomseddon/traefik-forward-auth:2
name: traefik-forward-auth
ports:
- containerPort: 4181
protocol: TCP
env:
- name: CONFIG
value: "/config"
- name: DOMAIN
value: "example.com"
# INSECURE_COOKIE is required if not using a https entrypoint
# - name: INSECURE_COOKIE
# value: "true"
# Remove COOKIE_DOMAIN if not using auth host mode
- name: COOKIE_DOMAIN
value: "example.com"
- name: AUTH_HOST
value: "auth.example.com"
- name: LOG_LEVEL
value: "info"
- name: PROVIDERS_GOOGLE_CLIENT_ID
valueFrom:
secretKeyRef:
name: traefik-forward-auth-secrets
key: google-client-id
- name: PROVIDERS_GOOGLE_CLIENT_SECRET
valueFrom:
secretKeyRef:
name: traefik-forward-auth-secrets
key: google-client-secret
- name: SECRET
valueFrom:
secretKeyRef:
name: traefik-forward-auth-secrets
key: secret
volumeMounts:
- name: configs
mountPath: /config
subPath: traefik-forward-auth.ini
volumes:
- name: configs
configMap:
name: configs
- name: traefik-forward-auth-secrets
secret:
secretName: traefik-forward-auth-secrets
- name: acme
persistentVolumeClaim:
claimName: traefik-acme

View File

@ -0,0 +1,42 @@
#
# Auth Ingress
#
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
name: traefik-forward-auth
labels:
app: traefik
spec:
entryPoints:
- https
routes:
- match: Host(`auth.example.com`)
kind: Rule
services:
- name: traefik-forward-auth
port: 4181
tls:
certresolver: default
---
#
# Dash Ingress
#
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
name: traefik-dashboard
labels:
app: traefik
spec:
entryPoints:
- https
routes:
- match: Host(`traefik.example.com`)
kind: Rule
services:
- name: api@internal
kind: TraefikService
tls:
certresolver: default

View File

@ -0,0 +1,29 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: default
commonLabels:
app: traefik
resources:
- crds.yaml
- deployment.yaml
- service.yaml
- ingress.yaml
- middleware.yaml
- pvc.yaml
- rbac.yaml
#
# Configs
#
configMapGenerator:
- name: configs
files:
- configs/traefik-forward-auth.ini
#
# Secrets
#
secretGenerator:
- name: traefik-forward-auth-secrets
env: secrets/traefik-forward-auth.env

View File

@ -0,0 +1,9 @@
apiVersion: traefik.containo.us/v1alpha1
kind: Middleware
metadata:
name: traefik-forward-auth
spec:
forwardAuth:
address: http://127.0.0.1:4181
authResponseHeaders:
- X-Forwarded-User

View File

@ -0,0 +1,17 @@
# Source: traefik/templates/acme-pvc.yaml
#
# PVC
#
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: traefik-acme
labels:
app: traefik
spec:
accessModes:
- "ReadWriteOnce"
resources:
requests:
storage: "1Gi"
storageClassName: "local-traefik-acme"

View File

@ -0,0 +1,66 @@
#
# RBAC
#
apiVersion: v1
kind: ServiceAccount
metadata:
namespace: default
name: traefik-ingress-controller
---
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata:
name: traefik-ingress-controller
rules:
- apiGroups:
- ""
resources:
- services
- endpoints
- secrets
verbs:
- get
- list
- watch
- apiGroups:
- extensions
resources:
- ingresses
verbs:
- get
- list
- watch
- apiGroups:
- extensions
resources:
- ingresses/status
verbs:
- update
- apiGroups:
- traefik.containo.us
resources:
- middlewares
- ingressroutes
- traefikservices
- ingressroutetcps
- ingressrouteudps
- tlsoptions
- tlsstores
verbs:
- get
- list
- watch
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata:
name: traefik-ingress-controller
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: traefik-ingress-controller
subjects:
- kind: ServiceAccount
name: traefik-ingress-controller
namespace: default

View File

@ -0,0 +1,3 @@
google-client-id=client-id
google-client-secret=client-secret
secret=something-random

View File

@ -0,0 +1,39 @@
#
# Traefik Service
#
apiVersion: v1
kind: Service
metadata:
name: traefik
labels:
app: traefik
spec:
# Use NodePort if required
type: LoadBalancer
selector:
app: traefik
ports:
- name: http
port: 80
targetPort: 80
- name: https
port: 443
targetPort: 443
---
#
# Auth Service
#
apiVersion: v1
kind: Service
metadata:
name: traefik-forward-auth
labels:
app: traefik
spec:
type: ClusterIP
selector:
app: traefik
ports:
- name: auth-http
port: 4181
targetPort: 4181

View File

@ -0,0 +1,19 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: whoami
labels:
app: whoami
spec:
replicas: 1
selector:
matchLabels:
app: whoami
template:
metadata:
labels:
app: whoami
spec:
containers:
- image: containous/whoami
name: whoami

View File

@ -0,0 +1,17 @@
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
name: whoami
labels:
app: whoami
spec:
entryPoints:
- https
routes:
- match: Host(`whoami.example.com`)
kind: Rule
services:
- name: whoami
port: 80
tls:
certresolver: default

View File

@ -0,0 +1,7 @@
commonLabels:
app: whoami
resources:
- deployment.yaml
- service.yaml
- ingress.yaml

View File

@ -0,0 +1,14 @@
apiVersion: v1
kind: Service
metadata:
name: whoami
labels:
app: whoami
spec:
type: ClusterIP
ports:
- name: http
port: 80
selector:
app: whoami

View File

@ -0,0 +1,39 @@
# Kubernetes - Simple Separate Pod Example
This is a simple example of how to deploy traefik-forward-auth in it's own pod with minimal configuration. This example is a good starting point for those who already have traefik deployed (e.g. using helm).
This example uses [Selective Authentication](https://github.com/thomseddon/traefik-forward-auth/blob/master/README.md#selective-ingress-authentication-in-kubernetes) to apply forward authentication to selected ingresses. This means ingresses will not be protected by default. Authentication can be applied by adding the `traefik-forward-auth` middleware, for example:
```
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
name: whoami
labels:
app: whoami
spec:
entryPoints:
- http
routes:
- match: Host(`whoami.example.com`)
kind: Rule
services:
- name: whoami
port: 80
middlewares:
- name: traefik-forward-auth
```
A minimal application example is provided in `k8s-app.yml`.
Example deployment:
```
# Deploy traefik-forward-auth
kubectl apply -f k8s-traefik-forward-auth.yml
# Deploy example whoami app
kubectl apply -f k8s-app.yml
```
Please see the advanced examples for more details.

View File

@ -0,0 +1,60 @@
#
# Example Application Deployment
#
apiVersion: apps/v1
kind: Deployment
metadata:
name: whoami
labels:
app: whoami
spec:
replicas: 1
selector:
matchLabels:
app: whoami
template:
metadata:
labels:
app: whoami
spec:
containers:
- name: whoami
image: containous/whoami
---
#
# Service
#
apiVersion: v1
kind: Service
metadata:
name: whoami
labels:
app: whoami
spec:
ports:
- name: http
port: 80
selector:
app: whoami
---
#
# IngressRoute
#
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
name: whoami
labels:
app: whoami
spec:
entryPoints:
- http
routes:
- match: Host(`whoami.example.com`)
kind: Rule
services:
- name: whoami
port: 80
middlewares:
- name: traefik-forward-auth

View File

@ -0,0 +1,104 @@
#
# Traefik Forward Auth Deployment
#
apiVersion: apps/v1
kind: Deployment
metadata:
name: traefik-forward-auth
labels:
app: traefik-forward-auth
spec:
replicas: 1
selector:
matchLabels:
app: traefik-forward-auth
strategy:
type: Recreate
template:
metadata:
labels:
app: traefik-forward-auth
spec:
terminationGracePeriodSeconds: 60
containers:
- image: thomseddon/traefik-forward-auth:2
name: traefik-forward-auth
ports:
- containerPort: 4181
protocol: TCP
env:
- name: DOMAIN
value: "example.com"
# INSECURE_COOKIE is required unless using https entrypoint
- name: INSECURE_COOKIE
value: "true"
- name: PROVIDERS_GOOGLE_CLIENT_ID
valueFrom:
secretKeyRef:
name: traefik-forward-auth-secrets
key: traefik-forward-auth-google-client-id
- name: PROVIDERS_GOOGLE_CLIENT_SECRET
valueFrom:
secretKeyRef:
name: traefik-forward-auth-secrets
key: traefik-forward-auth-google-client-secret
- name: SECRET
valueFrom:
secretKeyRef:
name: traefik-forward-auth-secrets
key: traefik-forward-auth-secret
---
#
# Auth Service
#
apiVersion: v1
kind: Service
metadata:
name: traefik-forward-auth
labels:
app: traefik-forward-auth
spec:
type: ClusterIP
selector:
app: traefik-forward-auth
ports:
- name: auth-http
port: 4181
targetPort: 4181
---
#
# Auth Middleware
#
apiVersion: traefik.containo.us/v1alpha1
kind: Middleware
metadata:
name: traefik-forward-auth
spec:
forwardAuth:
address: http://traefik-forward-auth:4181
authResponseHeaders:
- X-Forwarded-User
---
#
# Secrets
#
# Kubernetes requires secret values to be converted to base64 when defined
# explicitly like this. (use `echo -n 'secret-value' | base64`)
#
# These are here for completeness, in reality you may define these elsewhere,
# for example using kustomize (shown in advanced examples)
#
apiVersion: v1
kind: Secret
metadata:
name: traefik-forward-auth-secrets
labels:
app: traefik-forward-auth
type: Opaque
data:
traefik-forward-auth-google-client-id: base64-client-id
traefik-forward-auth-google-client-secret: base64-client-secret
traefik-forward-auth-secret: base64-something-random

View File

@ -0,0 +1,37 @@
version: '3'
services:
traefik:
image: traefik:v2.2
command:
- --providers.docker
# This example uses "global authentication"
- --entryPoints.http.address=:80
- --entrypoints.http.http.middlewares=traefik-forward-auth
ports:
- "8085:80"
- "8086:8080"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
whoami:
image: containous/whoami
labels:
- "traefik.http.routers.whoami.rule=Host(`whoami.localhost.com`)"
traefik-forward-auth:
image: thomseddon/traefik-forward-auth:2
environment:
- PROVIDERS_GOOGLE_CLIENT_ID=your-client-id
- PROVIDERS_GOOGLE_CLIENT_SECRET=your-client-secret
- SECRET=something-random
# INSECURE_COOKIE is required if not using a https entrypoint
- INSECURE_COOKIE=true
- COOKIE_DOMAIN=localhost.com
- AUTH_HOST=auth.localhost.com:8085
- LOG_LEVEL=debug
labels:
- "traefik.http.routers.traefik-forward-auth.rule=Host(`auth.localhost.com`)"
- "traefik.http.middlewares.traefik-forward-auth.forwardauth.address=http://traefik-forward-auth:4181"
- "traefik.http.middlewares.traefik-forward-auth.forwardauth.authResponseHeaders=X-Forwarded-User"
- "traefik.http.services.traefik-forward-auth.loadbalancer.server.port=4181"

View File

@ -0,0 +1,34 @@
version: '3'
services:
traefik:
image: traefik:v2.2
command: --providers.docker
ports:
- "8085:80"
- "8086:8080"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
whoami:
image: containous/whoami
labels:
- "traefik.http.routers.whoami.rule=Host(`whoami.localhost.com`)"
- "traefik.http.routers.whoami.middlewares=traefik-forward-auth"
traefik-forward-auth:
image: thomseddon/traefik-forward-auth:2
environment:
- DEFAULT_PROVIDER=oidc
- PROVIDERS_OIDC_ISSUER_URL=https://login.microsoftonline.com/{tenant}
- PROVIDERS_OIDC_CLIENT_ID=your-client-id
- PROVIDERS_OIDC_CLIENT_SECRET=your-client-secret
- SECRET=something-random
# INSECURE_COOKIE is required if not using a https entrypoint
- INSECURE_COOKIE=true
- LOG_LEVEL=debug
labels:
- "traefik.http.middlewares.traefik-forward-auth.forwardauth.address=http://traefik-forward-auth:4181"
- "traefik.http.middlewares.traefik-forward-auth.forwardauth.authResponseHeaders=X-Forwarded-User"
- "traefik.http.services.traefik-forward-auth.loadbalancer.server.port=4181"

View File

@ -0,0 +1,32 @@
version: '3'
services:
traefik:
image: traefik:v2.2
command: --providers.docker
ports:
- "8085:80"
- "8086:8080"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
whoami:
image: containous/whoami
labels:
- "traefik.http.routers.whoami.rule=Host(`whoami.localhost.com`)"
# This example uses "Selective Authentication"
- "traefik.http.routers.whoami.middlewares=traefik-forward-auth"
traefik-forward-auth:
image: thomseddon/traefik-forward-auth:2
environment:
- PROVIDERS_GOOGLE_CLIENT_ID=your-client-id
- PROVIDERS_GOOGLE_CLIENT_SECRET=your-client-secret
- SECRET=something-random
# INSECURE_COOKIE is required if not using a https entrypoint
- INSECURE_COOKIE=true
- LOG_LEVEL=debug
labels:
- "traefik.http.middlewares.traefik-forward-auth.forwardauth.address=http://traefik-forward-auth:4181"
- "traefik.http.middlewares.traefik-forward-auth.forwardauth.authResponseHeaders=X-Forwarded-User"
- "traefik.http.services.traefik-forward-auth.loadbalancer.server.port=4181"

View File

@ -17,6 +17,7 @@ import (
// Request Validation
// ValidateCookie verifies that a cookie matches the expected format of:
// Cookie = hash(secret, cookie domain, email, expires)|expires|email
func ValidateCookie(r *http.Request, c *http.Cookie) (string, error) {
parts := strings.Split(c.Value, "|")
@ -55,30 +56,43 @@ func ValidateCookie(r *http.Request, c *http.Cookie) (string, error) {
return parts[2], nil
}
// Validate email
// ValidateEmail checks if the given email address matches either a whitelisted
// email address, as defined by the "whitelist" config parameter. Or is part of
// a permitted domain, as defined by the "domains" config parameter
func ValidateEmail(email string) bool {
found := false
// Do we have any validation to perform?
if len(config.Whitelist) == 0 && len(config.Domains) == 0 {
return true
}
// Email whitelist validation
if len(config.Whitelist) > 0 {
for _, whitelist := range config.Whitelist {
if email == whitelist {
found = true
return true
}
}
} else if len(config.Domains) > 0 {
// If we're not matching *either*, stop here
if !config.MatchWhitelistOrDomain {
return false
}
}
// Domain validation
if len(config.Domains) > 0 {
parts := strings.Split(email, "@")
if len(parts) < 2 {
return false
}
for _, domain := range config.Domains {
if domain == parts[1] {
found = true
return true
}
}
} else {
return true
}
return found
return false
}
// Utility methods
@ -126,7 +140,7 @@ func useAuthDomain(r *http.Request) (bool, string) {
// Cookie methods
// Create an auth cookie
// MakeCookie creates an auth cookie
func MakeCookie(r *http.Request, email string) *http.Cookie {
expires := cookieExpiry()
mac := cookieSignature(r, email, fmt.Sprintf("%d", expires.Unix()))
@ -143,7 +157,20 @@ func MakeCookie(r *http.Request, email string) *http.Cookie {
}
}
// Make a CSRF cookie (used during login only)
// ClearCookie clears the auth cookie
func ClearCookie(r *http.Request) *http.Cookie {
return &http.Cookie{
Name: config.CookieName,
Value: "",
Path: "/",
Domain: cookieDomain(r),
HttpOnly: true,
Secure: !config.InsecureCookie,
Expires: time.Now().Local().Add(time.Hour * -1),
}
}
// MakeCSRFCookie makes a csrf cookie (used during login only)
func MakeCSRFCookie(r *http.Request, nonce string) *http.Cookie {
return &http.Cookie{
Name: config.CSRFCookieName,
@ -156,7 +183,7 @@ func MakeCSRFCookie(r *http.Request, nonce string) *http.Cookie {
}
}
// Create a cookie to clear csrf cookie
// ClearCSRFCookie makes an expired csrf cookie to clear csrf cookie
func ClearCSRFCookie(r *http.Request) *http.Cookie {
return &http.Cookie{
Name: config.CSRFCookieName,
@ -169,7 +196,7 @@ func ClearCSRFCookie(r *http.Request) *http.Cookie {
}
}
// Validate the csrf cookie against state
// ValidateCSRFCookie validates the csrf cookie against state
func ValidateCSRFCookie(r *http.Request, c *http.Cookie) (valid bool, provider string, redirect string, err error) {
state := r.URL.Query().Get("state")
@ -197,12 +224,13 @@ func ValidateCSRFCookie(r *http.Request, c *http.Cookie) (valid bool, provider s
return true, params[:split], params[split+1:], nil
}
// MakeState generates a state value
func MakeState(r *http.Request, p provider.Provider, nonce string) string {
return fmt.Sprintf("%s:%s:%s", nonce, p.Name(), returnUrl(r))
}
// Nonce generates a random nonce
func Nonce() (error, string) {
// Make nonce
nonce := make([]byte, 16)
_, err := rand.Read(nonce)
if err != nil {
@ -263,9 +291,7 @@ func cookieExpiry() time.Time {
return time.Now().Local().Add(config.Lifetime)
}
// Cookie Domain
// Cookie Domain
// CookieDomain holds cookie domain info
type CookieDomain struct {
Domain string
DomainLen int
@ -273,6 +299,7 @@ type CookieDomain struct {
SubDomainLen int
}
// NewCookieDomain creates a new CookieDomain from the given domain string
func NewCookieDomain(domain string) *CookieDomain {
return &CookieDomain{
Domain: domain,
@ -282,6 +309,7 @@ func NewCookieDomain(domain string) *CookieDomain {
}
}
// Match checks if the given host matches this CookieDomain
func (c *CookieDomain) Match(host string) bool {
// Exact domain match?
if host == c.Domain {
@ -296,19 +324,22 @@ func (c *CookieDomain) Match(host string) bool {
return false
}
// UnmarshalFlag converts a string to a CookieDomain
func (c *CookieDomain) UnmarshalFlag(value string) error {
*c = *NewCookieDomain(value)
return nil
}
// MarshalFlag converts a CookieDomain to a string
func (c *CookieDomain) MarshalFlag() (string, error) {
return c.Domain, nil
}
// Legacy support for comma separated list of cookie domains
// CookieDomains provides legacy sypport for comma separated list of cookie domains
type CookieDomains []CookieDomain
// UnmarshalFlag converts a comma separated list of cookie domains to an array
// of CookieDomains
func (c *CookieDomains) UnmarshalFlag(value string) error {
if len(value) > 0 {
for _, d := range strings.Split(value, ",") {
@ -319,6 +350,7 @@ func (c *CookieDomains) UnmarshalFlag(value string) error {
return nil
}
// MarshalFlag converts an array of CookieDomain to a comma seperated list
func (c *CookieDomains) MarshalFlag() (string, error) {
var domains []string
for _, d := range *c {

View File

@ -93,6 +93,30 @@ func TestAuthValidateEmail(t *testing.T) {
config.Whitelist = []string{"test@test.com"}
v = ValidateEmail("test@test.com")
assert.True(v, "should allow user in whitelist")
// Should allow only matching email address when
// MatchWhitelistOrDomain is disabled
config.Domains = []string{"example.com"}
config.Whitelist = []string{"test@test.com"}
config.MatchWhitelistOrDomain = false
v = ValidateEmail("test@test.com")
assert.True(v, "should allow user in whitelist")
v = ValidateEmail("test@example.com")
assert.False(v, "should not allow user from valid domain")
v = ValidateEmail("one@two.com")
assert.False(v, "should not allow user not in either")
// Should allow either matching domain or email address when
// MatchWhitelistOrDomain is enabled
config.Domains = []string{"example.com"}
config.Whitelist = []string{"test@test.com"}
config.MatchWhitelistOrDomain = true
v = ValidateEmail("test@test.com")
assert.True(v, "should allow user in whitelist")
v = ValidateEmail("test@example.com")
assert.True(v, "should allow user from valid domain")
v = ValidateEmail("one@two.com")
assert.False(v, "should not allow user not in either")
}
func TestRedirectUri(t *testing.T) {
@ -292,6 +316,11 @@ func TestMakeState(t *testing.T) {
p2 := provider.OIDC{}
state = MakeState(r, &p2, "nonce")
assert.Equal("nonce:oidc:http://example.com/hello", state)
// Test with Generic OAuth
p3 := provider.GenericOAuth{}
state = MakeState(r, &p3, "nonce")
assert.Equal("nonce:generic-oauth:http://example.com/hello", state)
}
func TestAuthNonce(t *testing.T) {

View File

@ -19,23 +19,26 @@ import (
var config *Config
// Config holds the runtime application config
type Config struct {
LogLevel string `long:"log-level" env:"LOG_LEVEL" default:"warn" choice:"trace" choice:"debug" choice:"info" choice:"warn" choice:"error" choice:"fatal" choice:"panic" description:"Log level"`
LogFormat string `long:"log-format" env:"LOG_FORMAT" default:"text" choice:"text" choice:"json" choice:"pretty" description:"Log format"`
AuthHost string `long:"auth-host" env:"AUTH_HOST" description:"Single host to use when returning from 3rd party auth"`
Config func(s string) error `long:"config" env:"CONFIG" description:"Path to config file" json:"-"`
CookieDomains []CookieDomain `long:"cookie-domain" env:"COOKIE_DOMAIN" description:"Domain to set auth cookie on, can be set multiple times"`
InsecureCookie bool `long:"insecure-cookie" env:"INSECURE_COOKIE" description:"Use insecure cookies"`
CookieName string `long:"cookie-name" env:"COOKIE_NAME" default:"_forward_auth" description:"Cookie Name"`
CSRFCookieName string `long:"csrf-cookie-name" env:"CSRF_COOKIE_NAME" default:"_forward_auth_csrf" description:"CSRF Cookie Name"`
DefaultAction string `long:"default-action" env:"DEFAULT_ACTION" default:"auth" choice:"auth" choice:"allow" description:"Default action"`
DefaultProvider string `long:"default-provider" env:"DEFAULT_PROVIDER" default:"google" choice:"google" choice:"oidc" description:"Default provider"`
Domains CommaSeparatedList `long:"domain" env:"DOMAIN" description:"Only allow given email domains, can be set multiple times"`
LifetimeString int `long:"lifetime" env:"LIFETIME" default:"43200" description:"Lifetime in seconds"`
Path string `long:"url-path" env:"URL_PATH" default:"/_oauth" description:"Callback URL Path"`
SecretString string `long:"secret" env:"SECRET" description:"Secret used for signing (required)" json:"-"`
Whitelist CommaSeparatedList `long:"whitelist" env:"WHITELIST" description:"Only allow given email addresses, can be set multiple times"`
AuthHost string `long:"auth-host" env:"AUTH_HOST" description:"Single host to use when returning from 3rd party auth"`
Config func(s string) error `long:"config" env:"CONFIG" description:"Path to config file" json:"-"`
CookieDomains []CookieDomain `long:"cookie-domain" env:"COOKIE_DOMAIN" env-delim:"," description:"Domain to set auth cookie on, can be set multiple times"`
InsecureCookie bool `long:"insecure-cookie" env:"INSECURE_COOKIE" description:"Use insecure cookies"`
CookieName string `long:"cookie-name" env:"COOKIE_NAME" default:"_forward_auth" description:"Cookie Name"`
CSRFCookieName string `long:"csrf-cookie-name" env:"CSRF_COOKIE_NAME" default:"_forward_auth_csrf" description:"CSRF Cookie Name"`
DefaultAction string `long:"default-action" env:"DEFAULT_ACTION" default:"auth" choice:"auth" choice:"allow" description:"Default action"`
DefaultProvider string `long:"default-provider" env:"DEFAULT_PROVIDER" default:"google" choice:"google" choice:"oidc" choice:"generic-oauth" description:"Default provider"`
Domains CommaSeparatedList `long:"domain" env:"DOMAIN" env-delim:"," description:"Only allow given email domains, can be set multiple times"`
LifetimeString int `long:"lifetime" env:"LIFETIME" default:"43200" description:"Lifetime in seconds"`
LogoutRedirect string `long:"logout-redirect" env:"LOGOUT_REDIRECT" description:"URL to redirect to following logout"`
MatchWhitelistOrDomain bool `long:"match-whitelist-or-domain" env:"MATCH_WHITELIST_OR_DOMAIN" description:"Allow users that match *either* whitelist or domain (enabled by default in v3)"`
Path string `long:"url-path" env:"URL_PATH" default:"/_oauth" description:"Callback URL Path"`
SecretString string `long:"secret" env:"SECRET" description:"Secret used for signing (required)" json:"-"`
Whitelist CommaSeparatedList `long:"whitelist" env:"WHITELIST" env-delim:"," description:"Only allow given email addresses, can be set multiple times"`
Providers provider.Providers `group:"providers" namespace:"providers" env-namespace:"PROVIDERS"`
Rules map[string]*Rule `long:"rule.<name>.<param>" description:"Rule definitions, param can be: \"action\", \"rule\" or \"provider\""`
@ -53,6 +56,7 @@ type Config struct {
PromptLegacy string `long:"prompt" env:"PROMPT" description:"DEPRECATED - Use \"providers.google.prompt\""`
}
// NewGlobalConfig creates a new global config, parsed from command arguments
func NewGlobalConfig() *Config {
var err error
config, err = NewConfig(os.Args[1:])
@ -66,6 +70,7 @@ func NewGlobalConfig() *Config {
// TODO: move config parsing into new func "NewParsedConfig"
// NewConfig parses and validates provided configuration into a config object
func NewConfig(args []string) (*Config, error) {
c := &Config{
Rules: map[string]*Rule{},
@ -236,6 +241,7 @@ func convertLegacyToIni(name string) (io.Reader, error) {
return bytes.NewReader(legacyFileFormat.ReplaceAll(b, []byte("$1=$2"))), nil
}
// Validate validates a config object
func (c *Config) Validate() {
// Check for show stopper errors
if len(c.Secret) == 0 {
@ -269,6 +275,8 @@ func (c *Config) GetProvider(name string) (provider.Provider, error) {
return &c.Providers.Google, nil
case "oidc":
return &c.Providers.OIDC, nil
case "generic-oauth":
return &c.Providers.GenericOAuth, nil
}
return nil, fmt.Errorf("Unknown provider: %s", name)
@ -317,12 +325,14 @@ func (c *Config) setupProvider(name string) error {
return nil
}
// Rule holds defined rules
type Rule struct {
Action string
Rule string
Provider string
}
// NewRule creates a new rule object
func NewRule() *Rule {
return &Rule{
Action: "auth",
@ -335,6 +345,7 @@ func (r *Rule) formattedRule() string {
return strings.ReplaceAll(r.Rule, "Host(", "HostRegexp(")
}
// Validate validates a rule
func (r *Rule) Validate(c *Config) error {
if r.Action != "auth" && r.Action != "allow" {
return errors.New("invalid rule action, must be \"auth\" or \"allow\"")
@ -345,13 +356,16 @@ func (r *Rule) Validate(c *Config) error {
// Legacy support for comma separated lists
// CommaSeparatedList provides legacy support for config values provided as csv
type CommaSeparatedList []string
// UnmarshalFlag converts a comma separated list to an array
func (c *CommaSeparatedList) UnmarshalFlag(value string) error {
*c = append(*c, strings.Split(value, ",")...)
return nil
}
// MarshalFlag converts an array back to a comma separated list
func (c *CommaSeparatedList) MarshalFlag() (string, error) {
return strings.Join(*c, ","), nil
}

View File

@ -33,8 +33,12 @@ func TestConfigDefaults(t *testing.T) {
assert.Equal("google", c.DefaultProvider)
assert.Len(c.Domains, 0)
assert.Equal(time.Second*time.Duration(43200), c.Lifetime)
assert.Equal("", c.LogoutRedirect)
assert.False(c.MatchWhitelistOrDomain)
assert.Equal("/_oauth", c.Path)
assert.Len(c.Whitelist, 0)
assert.Equal("select_account", c.Providers.Google.Prompt)
}
func TestConfigParseArgs(t *testing.T) {
@ -197,14 +201,27 @@ func TestConfigParseEnvironment(t *testing.T) {
assert := assert.New(t)
os.Setenv("COOKIE_NAME", "env_cookie_name")
os.Setenv("PROVIDERS_GOOGLE_CLIENT_ID", "env_client_id")
os.Setenv("COOKIE_DOMAIN", "test1.com,example.org")
os.Setenv("DOMAIN", "test2.com,example.org")
os.Setenv("WHITELIST", "test3.com,example.org")
c, err := NewConfig([]string{})
assert.Nil(err)
assert.Equal("env_cookie_name", c.CookieName, "variable should be read from environment")
assert.Equal("env_client_id", c.Providers.Google.ClientID, "namespace variable should be read from environment")
assert.Equal([]CookieDomain{
*NewCookieDomain("test1.com"),
*NewCookieDomain("example.org"),
}, c.CookieDomains, "array variable should be read from environment COOKIE_DOMAIN")
assert.Equal(CommaSeparatedList{"test2.com", "example.org"}, c.Domains, "array variable should be read from environment DOMAIN")
assert.Equal(CommaSeparatedList{"test3.com", "example.org"}, c.Whitelist, "array variable should be read from environment WHITELIST")
os.Unsetenv("COOKIE_NAME")
os.Unsetenv("PROVIDERS_GOOGLE_CLIENT_ID")
os.Unsetenv("COOKIE_DOMAIN")
os.Unsetenv("DOMAIN")
os.Unsetenv("WHITELIST")
}
func TestConfigParseEnvironmentBackwardsCompatability(t *testing.T) {
@ -349,6 +366,11 @@ func TestConfigGetProvider(t *testing.T) {
assert.Nil(err)
assert.Equal(&c.Providers.OIDC, p)
// Should be able to get "generic-oauth" provider
p, err = c.GetProvider("generic-oauth")
assert.Nil(err)
assert.Equal(&c.Providers.GenericOAuth, p)
// Should catch unknown provider
p, err = c.GetProvider("bad")
if assert.Error(err) {

View File

@ -8,6 +8,7 @@ import (
var log *logrus.Logger
// NewDefaultLogger creates a new logger based on the current configuration
func NewDefaultLogger() *logrus.Logger {
// Setup logger
log = logrus.StandardLogger()

View File

@ -0,0 +1,96 @@
package provider
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"golang.org/x/oauth2"
)
// GenericOAuth provider
type GenericOAuth struct {
AuthURL string `long:"auth-url" env:"AUTH_URL" description:"Auth/Login URL"`
TokenURL string `long:"token-url" env:"TOKEN_URL" description:"Token URL"`
UserURL string `long:"user-url" env:"USER_URL" description:"URL used to retrieve user info"`
ClientID string `long:"client-id" env:"CLIENT_ID" description:"Client ID"`
ClientSecret string `long:"client-secret" env:"CLIENT_SECRET" description:"Client Secret" json:"-"`
Scopes []string `long:"scope" env:"SCOPE" env-delim:"," default:"profile" default:"email" description:"Scopes"`
TokenStyle string `long:"token-style" env:"TOKEN_STYLE" default:"header" choice:"header" choice:"query" description:"How token is presented when querying the User URL"`
OAuthProvider
}
// Name returns the name of the provider
func (o *GenericOAuth) Name() string {
return "generic-oauth"
}
// Setup performs validation and setup
func (o *GenericOAuth) Setup() error {
// Check parmas
if o.AuthURL == "" || o.TokenURL == "" || o.UserURL == "" || o.ClientID == "" || o.ClientSecret == "" {
return errors.New("providers.generic-oauth.auth-url, providers.generic-oauth.token-url, providers.generic-oauth.user-url, providers.generic-oauth.client-id, providers.generic-oauth.client-secret must be set")
}
// Create oauth2 config
o.Config = &oauth2.Config{
ClientID: o.ClientID,
ClientSecret: o.ClientSecret,
Endpoint: oauth2.Endpoint{
AuthURL: o.AuthURL,
TokenURL: o.TokenURL,
},
Scopes: o.Scopes,
}
o.ctx = context.Background()
return nil
}
// GetLoginURL provides the login url for the given redirect uri and state
func (o *GenericOAuth) GetLoginURL(redirectURI, state string) string {
return o.OAuthGetLoginURL(redirectURI, state)
}
// ExchangeCode exchanges the given redirect uri and code for a token
func (o *GenericOAuth) ExchangeCode(redirectURI, code string) (string, error) {
token, err := o.OAuthExchangeCode(redirectURI, code)
if err != nil {
return "", err
}
return token.AccessToken, nil
}
// GetUser uses the given token and returns a complete provider.User object
func (o *GenericOAuth) GetUser(token string) (User, error) {
var user User
req, err := http.NewRequest("GET", o.UserURL, nil)
if err != nil {
return user, err
}
if o.TokenStyle == "header" {
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
} else if o.TokenStyle == "query" {
q := req.URL.Query()
q.Add("access_token", token)
req.URL.RawQuery = q.Encode()
}
client := &http.Client{}
res, err := client.Do(req)
if err != nil {
return user, err
}
defer res.Body.Close()
err = json.NewDecoder(res.Body).Decode(&user)
return user, err
}

View File

@ -0,0 +1,140 @@
package provider
import (
"net/url"
"testing"
"github.com/stretchr/testify/assert"
"golang.org/x/oauth2"
)
// Tests
func TestGenericOAuthName(t *testing.T) {
p := GenericOAuth{}
assert.Equal(t, "generic-oauth", p.Name())
}
func TestGenericOAuthSetup(t *testing.T) {
assert := assert.New(t)
p := GenericOAuth{}
// Check validation
err := p.Setup()
if assert.Error(err) {
assert.Equal("providers.generic-oauth.auth-url, providers.generic-oauth.token-url, providers.generic-oauth.user-url, providers.generic-oauth.client-id, providers.generic-oauth.client-secret must be set", err.Error())
}
// Check setup
p = GenericOAuth{
AuthURL: "https://provider.com/oauth2/auth",
TokenURL: "https://provider.com/oauth2/token",
UserURL: "https://provider.com/oauth2/user",
ClientID: "id",
ClientSecret: "secret",
}
err = p.Setup()
assert.Nil(err)
}
func TestGenericOAuthGetLoginURL(t *testing.T) {
assert := assert.New(t)
p := GenericOAuth{
AuthURL: "https://provider.com/oauth2/auth",
TokenURL: "https://provider.com/oauth2/token",
UserURL: "https://provider.com/oauth2/user",
ClientID: "idtest",
ClientSecret: "secret",
Scopes: []string{"scopetest"},
}
err := p.Setup()
if err != nil {
t.Fatal(err)
}
// Check url
uri, err := url.Parse(p.GetLoginURL("http://example.com/_oauth", "state"))
assert.Nil(err)
assert.Equal("https", uri.Scheme)
assert.Equal("provider.com", uri.Host)
assert.Equal("/oauth2/auth", uri.Path)
// Check query string
qs := uri.Query()
expectedQs := url.Values{
"client_id": []string{"idtest"},
"redirect_uri": []string{"http://example.com/_oauth"},
"response_type": []string{"code"},
"scope": []string{"scopetest"},
"state": []string{"state"},
}
assert.Equal(expectedQs, qs)
}
func TestGenericOAuthExchangeCode(t *testing.T) {
assert := assert.New(t)
// Setup server
expected := url.Values{
"client_id": []string{"idtest"},
"client_secret": []string{"sectest"},
"code": []string{"code"},
"grant_type": []string{"authorization_code"},
"redirect_uri": []string{"http://example.com/_oauth"},
}
server, serverURL := NewOAuthServer(t, map[string]string{
"token": expected.Encode(),
})
defer server.Close()
// Setup provider
p := GenericOAuth{
AuthURL: "https://provider.com/oauth2/auth",
TokenURL: serverURL.String() + "/token",
UserURL: "https://provider.com/oauth2/user",
ClientID: "idtest",
ClientSecret: "sectest",
}
err := p.Setup()
if err != nil {
t.Fatal(err)
}
// We force AuthStyleInParams to prevent the test failure when the
// AuthStyleInHeader is attempted
p.Config.Endpoint.AuthStyle = oauth2.AuthStyleInParams
token, err := p.ExchangeCode("http://example.com/_oauth", "code")
assert.Nil(err)
assert.Equal("123456789", token)
}
func TestGenericOAuthGetUser(t *testing.T) {
assert := assert.New(t)
// Setup server
server, serverURL := NewOAuthServer(t, nil)
defer server.Close()
// Setup provider
p := GenericOAuth{
AuthURL: "https://provider.com/oauth2/auth",
TokenURL: "https://provider.com/oauth2/token",
UserURL: serverURL.String() + "/userinfo",
ClientID: "idtest",
ClientSecret: "sectest",
}
err := p.Setup()
if err != nil {
t.Fatal(err)
}
// We force AuthStyleInParams to prevent the test failure when the
// AuthStyleInHeader is attempted
p.Config.Endpoint.AuthStyle = oauth2.AuthStyleInParams
user, err := p.GetUser("123456789")
assert.Nil(err)
assert.Equal("example@example.com", user.Email)
}

View File

@ -13,7 +13,7 @@ type Google struct {
ClientID string `long:"client-id" env:"CLIENT_ID" description:"Client ID"`
ClientSecret string `long:"client-secret" env:"CLIENT_SECRET" description:"Client Secret" json:"-"`
Scope string
Prompt string `long:"prompt" env:"PROMPT" description:"Space separated list of OpenID prompt options"`
Prompt string `long:"prompt" env:"PROMPT" default:"select_account" description:"Space separated list of OpenID prompt options"`
LoginURL *url.URL
TokenURL *url.URL

View File

@ -144,8 +144,5 @@ func TestGoogleGetUser(t *testing.T) {
user, err := p.GetUser("123456789")
assert.Nil(err)
assert.Equal("1", user.ID)
assert.Equal("example@example.com", user.Email)
assert.True(user.Verified)
assert.Equal("example.com", user.Hd)
}

View File

@ -10,12 +10,12 @@ import (
// OIDC provider
type OIDC struct {
OAuthProvider
IssuerURL string `long:"issuer-url" env:"ISSUER_URL" description:"Issuer URL"`
ClientID string `long:"client-id" env:"CLIENT_ID" description:"Client ID"`
ClientSecret string `long:"client-secret" env:"CLIENT_SECRET" description:"Client Secret" json:"-"`
OAuthProvider
provider *oidc.Provider
verifier *oidc.IDTokenVerifier
}
@ -91,18 +91,9 @@ func (o *OIDC) GetUser(token string) (User, error) {
}
// Extract custom claims
var claims struct {
ID string `json:"sub"`
Email string `json:"email"`
Verified bool `json:"email_verified"`
}
if err := idToken.Claims(&claims); err != nil {
if err := idToken.Claims(&user); err != nil {
return user, err
}
user.ID = claims.ID
user.Email = claims.Email
user.Verified = claims.Verified
return user, nil
}

View File

@ -59,6 +59,33 @@ func TestOIDCGetLoginURL(t *testing.T) {
// Calling the method should not modify the underlying config
assert.Equal("", provider.Config.RedirectURL)
//
// Test with resource config option
//
provider.Resource = "resourcetest"
// Check url
uri, err = url.Parse(provider.GetLoginURL("http://example.com/_oauth", "state"))
assert.Nil(err)
assert.Equal(serverURL.Scheme, uri.Scheme)
assert.Equal(serverURL.Host, uri.Host)
assert.Equal("/auth", uri.Path)
// Check query string
qs = uri.Query()
expectedQs = url.Values{
"client_id": []string{"idtest"},
"redirect_uri": []string{"http://example.com/_oauth"},
"response_type": []string{"code"},
"scope": []string{"openid profile email"},
"state": []string{"state"},
"resource": []string{"resourcetest"},
}
assert.Equal(expectedQs, qs)
// Calling the method should not modify the underlying config
assert.Equal("", provider.Config.RedirectURL)
}
func TestOIDCExchangeCode(t *testing.T) {
@ -97,9 +124,7 @@ func TestOIDCGetUser(t *testing.T) {
// Get user
user, err := provider.GetUser(token)
assert.Nil(err)
assert.Equal("1", user.ID)
assert.Equal("example@example.com", user.Email)
assert.True(user.Verified)
}
// Utils

View File

@ -9,8 +9,9 @@ import (
// Providers contains all the implemented providers
type Providers struct {
Google Google `group:"Google Provider" namespace:"google" env-namespace:"GOOGLE"`
OIDC OIDC `group:"OIDC Provider" namespace:"oidc" env-namespace:"OIDC"`
Google Google `group:"Google Provider" namespace:"google" env-namespace:"GOOGLE"`
OIDC OIDC `group:"OIDC Provider" namespace:"oidc" env-namespace:"OIDC"`
GenericOAuth GenericOAuth `group:"Generic OAuth2 Provider" namespace:"generic-oauth" env-namespace:"GENERIC_OAUTH"`
}
// Provider is used to authenticate users
@ -28,14 +29,13 @@ type token struct {
// User is the authenticated user
type User struct {
ID string `json:"id"`
Email string `json:"email"`
Verified bool `json:"verified_email"`
Hd string `json:"hd"`
Email string `json:"email"`
}
// OAuthProvider is a provider using the oauth2 library
type OAuthProvider struct {
Resource string `long:"resource" env:"RESOURCE" description:"Optional resource indicator"`
Config *oauth2.Config
ctx context.Context
}
@ -51,6 +51,11 @@ func (p *OAuthProvider) ConfigCopy(redirectURI string) oauth2.Config {
// OAuthGetLoginURL provides a base "GetLoginURL" for proiders using OAauth2
func (p *OAuthProvider) OAuthGetLoginURL(redirectURI, state string) string {
config := p.ConfigCopy(redirectURI)
if p.Resource != "" {
return config.AuthCodeURL(state, oauth2.SetAuthURLParam("resource", p.Resource))
}
return config.AuthCodeURL(state)
}

View File

@ -9,10 +9,12 @@ import (
"github.com/thomseddon/traefik-forward-auth/internal/provider"
)
// Server contains router and handler methods
type Server struct {
router *rules.Router
}
// NewServer creates a new server object and builds router
func NewServer() *Server {
s := &Server{}
s.buildRoutes()
@ -39,6 +41,9 @@ func (s *Server) buildRoutes() {
// Add callback handler
s.router.Handle(config.Path, s.AuthCallbackHandler())
// Add logout handler
s.router.Handle(config.Path+"/logout", s.LogoutHandler())
// Add a default handler
if config.DefaultAction == "allow" {
s.router.NewRoute().Handler(s.AllowHandler("default"))
@ -47,6 +52,8 @@ func (s *Server) buildRoutes() {
}
}
// RootHandler Overwrites the request method, host and URL with those from the
// forwarded request so it's correctly routed by mux
func (s *Server) RootHandler(w http.ResponseWriter, r *http.Request) {
// Modify request
r.Method = r.Header.Get("X-Forwarded-Method")
@ -57,21 +64,21 @@ func (s *Server) RootHandler(w http.ResponseWriter, r *http.Request) {
s.router.ServeHTTP(w, r)
}
// Handler that allows requests
// AllowHandler Allows requests
func (s *Server) AllowHandler(rule string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
s.logger(r, rule, "Allowing request")
s.logger(r, "Allow", rule, "Allowing request")
w.WriteHeader(200)
}
}
// Authenticate requests
// AuthHandler Authenticates requests
func (s *Server) AuthHandler(providerName, rule string) http.HandlerFunc {
p, _ := config.GetConfiguredProvider(providerName)
return func(w http.ResponseWriter, r *http.Request) {
// Logging setup
logger := s.logger(r, rule, "Authenticating request")
logger := s.logger(r, "Auth", rule, "Authenticating request")
// Get auth cookie
c, err := r.Cookie(config.CookieName)
@ -87,7 +94,7 @@ func (s *Server) AuthHandler(providerName, rule string) http.HandlerFunc {
logger.Info("Cookie has expired")
s.authRedirect(logger, w, r, p)
} else {
logger.Errorf("Invalid cookie: %v", err)
logger.WithField("error", err).Warn("Invalid cookie")
http.Error(w, "Not authorized", 401)
}
return
@ -96,30 +103,28 @@ func (s *Server) AuthHandler(providerName, rule string) http.HandlerFunc {
// Validate user
valid := ValidateEmail(email)
if !valid {
logger.WithFields(logrus.Fields{
"email": email,
}).Errorf("Invalid email")
logger.WithField("email", email).Warn("Invalid email")
http.Error(w, "Not authorized", 401)
return
}
// Valid request
logger.Debugf("Allowing valid request ")
logger.Debug("Allowing valid request")
w.Header().Set("X-Forwarded-User", email)
w.WriteHeader(200)
}
}
// Handle auth callback
// AuthCallbackHandler Handles auth callback request
func (s *Server) AuthCallbackHandler() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// Logging setup
logger := s.logger(r, "default", "Handling callback")
logger := s.logger(r, "AuthCallback", "default", "Handling callback")
// Check for CSRF cookie
c, err := r.Cookie(config.CSRFCookieName)
if err != nil {
logger.Warn("Missing csrf cookie")
logger.Info("Missing csrf cookie")
http.Error(w, "Not authorized", 401)
return
}
@ -127,7 +132,10 @@ func (s *Server) AuthCallbackHandler() http.HandlerFunc {
// Validate state
valid, providerName, redirect, err := ValidateCSRFCookie(r, c)
if !valid {
logger.Warnf("Error validating csrf cookie: %v", err)
logger.WithFields(logrus.Fields{
"error": err,
"csrf_cookie": c,
}).Warn("Error validating csrf cookie")
http.Error(w, "Not authorized", 401)
return
}
@ -135,7 +143,11 @@ func (s *Server) AuthCallbackHandler() http.HandlerFunc {
// Get provider
p, err := config.GetConfiguredProvider(providerName)
if err != nil {
logger.Warnf("Invalid provider in csrf cookie: %s, %v", providerName, err)
logger.WithFields(logrus.Fields{
"error": err,
"csrf_cookie": c,
"provider": providerName,
}).Warn("Invalid provider in csrf cookie")
http.Error(w, "Not authorized", 401)
return
}
@ -146,7 +158,7 @@ func (s *Server) AuthCallbackHandler() http.HandlerFunc {
// Exchange code for token
token, err := p.ExchangeCode(redirectUri(r), r.URL.Query().Get("code"))
if err != nil {
logger.Errorf("Code exchange failed with: %v", err)
logger.WithField("error", err).Error("Code exchange failed with provider")
http.Error(w, "Service unavailable", 503)
return
}
@ -154,52 +166,85 @@ func (s *Server) AuthCallbackHandler() http.HandlerFunc {
// Get user
user, err := p.GetUser(token)
if err != nil {
logger.Errorf("Error getting user: %s", err)
logger.WithField("error", err).Error("Error getting user")
http.Error(w, "Service unavailable", 503)
return
}
// Generate cookie
http.SetCookie(w, MakeCookie(r, user.Email))
logger.WithFields(logrus.Fields{
"user": user.Email,
}).Infof("Generated auth cookie")
"provider": providerName,
"redirect": redirect,
"user": user.Email,
}).Info("Successfully generated auth cookie, redirecting user.")
// Redirect
http.Redirect(w, r, redirect, http.StatusTemporaryRedirect)
}
}
// LogoutHandler logs a user out
func (s *Server) LogoutHandler() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// Clear cookie
http.SetCookie(w, ClearCookie(r))
logger := s.logger(r, "Logout", "default", "Handling logout")
logger.Info("Logged out user")
if config.LogoutRedirect != "" {
http.Redirect(w, r, config.LogoutRedirect, http.StatusTemporaryRedirect)
} else {
http.Error(w, "You have been logged out", 401)
}
}
}
func (s *Server) authRedirect(logger *logrus.Entry, w http.ResponseWriter, r *http.Request, p provider.Provider) {
// Error indicates no cookie, generate nonce
err, nonce := Nonce()
if err != nil {
logger.Errorf("Error generating nonce, %v", err)
logger.WithField("error", err).Error("Error generating nonce")
http.Error(w, "Service unavailable", 503)
return
}
// Set the CSRF cookie
http.SetCookie(w, MakeCSRFCookie(r, nonce))
logger.Debug("Set CSRF cookie and redirecting to google login")
csrf := MakeCSRFCookie(r, nonce)
http.SetCookie(w, csrf)
if !config.InsecureCookie && r.Header.Get("X-Forwarded-Proto") != "https" {
logger.Warn("You are using \"secure\" cookies for a request that was not " +
"received via https. You should either redirect to https or pass the " +
"\"insecure-cookie\" config option to permit cookies via http.")
}
// Forward them on
loginUrl := p.GetLoginURL(redirectUri(r), MakeState(r, p, nonce))
http.Redirect(w, r, loginUrl, http.StatusTemporaryRedirect)
loginURL := p.GetLoginURL(redirectUri(r), MakeState(r, p, nonce))
http.Redirect(w, r, loginURL, http.StatusTemporaryRedirect)
logger.Debug("Done")
return
logger.WithFields(logrus.Fields{
"csrf_cookie": csrf,
"login_url": loginURL,
}).Debug("Set CSRF cookie and redirected to provider login url")
}
func (s *Server) logger(r *http.Request, rule, msg string) *logrus.Entry {
func (s *Server) logger(r *http.Request, handler, rule, msg string) *logrus.Entry {
// Create logger
logger := log.WithFields(logrus.Fields{
"handler": handler,
"rule": rule,
"method": r.Header.Get("X-Forwarded-Method"),
"proto": r.Header.Get("X-Forwarded-Proto"),
"host": r.Header.Get("X-Forwarded-Host"),
"uri": r.Header.Get("X-Forwarded-Uri"),
"source_ip": r.Header.Get("X-Forwarded-For"),
})
// Log request
logger.WithFields(logrus.Fields{
"rule": rule,
"headers": r.Header,
"cookies": r.Cookies(),
}).Debug(msg)
return logger

View File

@ -10,6 +10,8 @@ import (
"testing"
"time"
"github.com/sirupsen/logrus"
"github.com/sirupsen/logrus/hooks/test"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/oauth2"
@ -32,6 +34,8 @@ func init() {
func TestServerAuthHandlerInvalid(t *testing.T) {
assert := assert.New(t)
config = newDefaultConfig()
var hook *test.Hook
log, hook = test.NewNullLogger()
// Should redirect vanilla request to login url
req := newDefaultHttpRequest("/foo")
@ -53,6 +57,14 @@ func TestServerAuthHandlerInvalid(t *testing.T) {
assert.Equal("google", parts[1])
assert.Equal("http://example.com/foo", parts[2])
// Should warn as using http without insecure cookie
logs := hook.AllEntries()
assert.Len(logs, 1)
assert.Equal("You are using \"secure\" cookies for a request that was not "+
"received via https. You should either redirect to https or pass the "+
"\"insecure-cookie\" config option to permit cookies via http.", logs[0].Message)
assert.Equal(logrus.WarnLevel, logs[0].Level)
// Should catch invalid cookie
req = newDefaultHttpRequest("/foo")
c := MakeCookie(req, "test@example.com")
@ -146,6 +158,12 @@ func TestServerAuthCallback(t *testing.T) {
res, _ = doHttpRequest(req, c)
assert.Equal(401, res.StatusCode, "auth callback with invalid cookie shouldn't be authorised")
// Should catch invalid provider cookie
req = newDefaultHttpRequest("/_oauth?state=12345678901234567890123456789012:invalid:http://redirect")
c = MakeCSRFCookie(req, "12345678901234567890123456789012")
res, _ = doHttpRequest(req, c)
assert.Equal(401, res.StatusCode, "auth callback with invalid provider shouldn't be authorised")
// Should redirect valid request
req = newDefaultHttpRequest("/_oauth?state=12345678901234567890123456789012:google:http://redirect")
c = MakeCSRFCookie(req, "12345678901234567890123456789012")
@ -158,6 +176,101 @@ func TestServerAuthCallback(t *testing.T) {
assert.Equal("", fwd.Path, "valid request should be redirected to return url")
}
func TestServerAuthCallbackExchangeFailure(t *testing.T) {
assert := assert.New(t)
config = newDefaultConfig()
// Setup OAuth server
server, serverURL := NewFailingOAuthServer(t)
defer server.Close()
config.Providers.Google.TokenURL = &url.URL{
Scheme: serverURL.Scheme,
Host: serverURL.Host,
Path: "/token",
}
config.Providers.Google.UserURL = &url.URL{
Scheme: serverURL.Scheme,
Host: serverURL.Host,
Path: "/userinfo",
}
// Should handle failed code exchange
req := newDefaultHttpRequest("/_oauth?state=12345678901234567890123456789012:google:http://redirect")
c := MakeCSRFCookie(req, "12345678901234567890123456789012")
res, _ := doHttpRequest(req, c)
assert.Equal(503, res.StatusCode, "auth callback should handle failed code exchange")
}
func TestServerAuthCallbackUserFailure(t *testing.T) {
assert := assert.New(t)
config = newDefaultConfig()
// Setup OAuth server
server, serverURL := NewOAuthServer(t)
defer server.Close()
config.Providers.Google.TokenURL = &url.URL{
Scheme: serverURL.Scheme,
Host: serverURL.Host,
Path: "/token",
}
serverFail, serverFailURL := NewFailingOAuthServer(t)
defer serverFail.Close()
config.Providers.Google.UserURL = &url.URL{
Scheme: serverFailURL.Scheme,
Host: serverFailURL.Host,
Path: "/userinfo",
}
// Should handle failed user request
req := newDefaultHttpRequest("/_oauth?state=12345678901234567890123456789012:google:http://redirect")
c := MakeCSRFCookie(req, "12345678901234567890123456789012")
res, _ := doHttpRequest(req, c)
assert.Equal(503, res.StatusCode, "auth callback should handle failed user request")
}
func TestServerLogout(t *testing.T) {
require := require.New(t)
assert := assert.New(t)
config = newDefaultConfig()
req := newDefaultHttpRequest("/_oauth/logout")
res, _ := doHttpRequest(req, nil)
require.Equal(401, res.StatusCode, "should return a 401")
// Check for cookie
var cookie *http.Cookie
for _, c := range res.Cookies() {
if c.Name == config.CookieName {
cookie = c
}
}
require.NotNil(cookie)
require.Less(cookie.Expires.Local().Unix(), time.Now().Local().Unix()-50, "cookie should have expired")
// Test with redirect
config.LogoutRedirect = "http://redirect/path"
req = newDefaultHttpRequest("/_oauth/logout")
res, _ = doHttpRequest(req, nil)
require.Equal(307, res.StatusCode, "should return a 307")
// Check for cookie
cookie = nil
for _, c := range res.Cookies() {
if c.Name == config.CookieName {
cookie = c
}
}
require.NotNil(cookie)
require.Less(cookie.Expires.Local().Unix(), time.Now().Local().Unix()-50, "cookie should have expired")
fwd, _ := res.Location()
require.NotNil(fwd)
assert.Equal("http", fwd.Scheme, "valid request should be redirected to return url")
assert.Equal("redirect", fwd.Host, "valid request should be redirected to return url")
assert.Equal("/path", fwd.Path, "valid request should be redirected to return url")
}
func TestServerDefaultAction(t *testing.T) {
assert := assert.New(t)
config = newDefaultConfig()
@ -343,10 +456,16 @@ func TestServerRouteQuery(t *testing.T) {
*/
type OAuthServer struct {
t *testing.T
t *testing.T
fail bool
}
func (s *OAuthServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if s.fail {
http.Error(w, "Service unavailable", 500)
return
}
if r.URL.Path == "/token" {
fmt.Fprintf(w, `{"access_token":"123456789"}`)
} else if r.URL.Path == "/userinfo" {
@ -368,6 +487,13 @@ func NewOAuthServer(t *testing.T) (*httptest.Server, *url.URL) {
return server, serverURL
}
func NewFailingOAuthServer(t *testing.T) (*httptest.Server, *url.URL) {
handler := &OAuthServer{fail: true}
server := httptest.NewServer(handler)
serverURL, _ := url.Parse(server.URL)
return server, serverURL
}
func doHttpRequest(r *http.Request, c *http.Cookie) (*http.Response, string) {
w := httptest.NewRecorder()