Compare commits
41 Commits
Author | SHA1 | Date | |
---|---|---|---|
e41e8e1a17
|
|||
86894f72ed
|
|||
3f9d70e87b
|
|||
fb24320552
|
|||
f6120640d2
|
|||
ab2d527dbd
|
|||
5828a9a5a2
|
|||
c4317b7503 | |||
4ffb6593d5 | |||
6c6f75e80d | |||
8be8244b13 | |||
f96a3fb332 | |||
c19f622fbd | |||
04f5499f0b | |||
41560feaa7 | |||
1743537438 | |||
9e5994b959 | |||
870724c994 | |||
be2b4ba9f4 | |||
529e28d83b | |||
2937b04fdb | |||
fb8b216481 | |||
8b3a950162 | |||
655eddeaf9 | |||
c63fd738d6 | |||
00b5d9e031 | |||
8902cf8735 | |||
3345f8ec69 | |||
60604ad3db | |||
a668454a11 | |||
eec62eb03a | |||
7381450015 | |||
f7a94e7db9 | |||
f802a366de | |||
07f9587bc1 | |||
1ac0ca9732 | |||
9abf5645b7 | |||
3a66191314 | |||
c3b4ba8244 | |||
b413c60d42 | |||
e678a33016 |
@ -1,2 +1,4 @@
|
|||||||
example
|
example
|
||||||
.travis.yml
|
.travis.yml
|
||||||
|
.git
|
||||||
|
.gitlab-ci.yml
|
||||||
|
6
.gitlab-ci.yml
Normal file
6
.gitlab-ci.yml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
include:
|
||||||
|
- project: dockerized/commons
|
||||||
|
ref: master
|
||||||
|
file: gitlab-ci-template.yml
|
||||||
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
|||||||
language: go
|
|
||||||
sudo: false
|
|
||||||
go:
|
|
||||||
- "1.12"
|
|
||||||
script: env GO111MODULE=on go test -v ./...
|
|
@ -1,18 +0,0 @@
|
|||||||
FROM golang:1.12-alpine as builder
|
|
||||||
|
|
||||||
# Setup
|
|
||||||
RUN mkdir -p /go/src/github.com/thomseddon/traefik-forward-auth
|
|
||||||
WORKDIR /go/src/github.com/thomseddon/traefik-forward-auth
|
|
||||||
|
|
||||||
# Add libraries
|
|
||||||
RUN apk add --no-cache git
|
|
||||||
|
|
||||||
# Copy & build
|
|
||||||
ADD . /go/src/github.com/thomseddon/traefik-forward-auth/
|
|
||||||
RUN CGO_ENABLED=0 GOOS=linux GOARCH=arm GO111MODULE=on go build -a -installsuffix nocgo -o /traefik-forward-auth github.com/thomseddon/traefik-forward-auth/cmd
|
|
||||||
|
|
||||||
# Copy into scratch container
|
|
||||||
FROM scratch
|
|
||||||
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
|
||||||
COPY --from=builder /traefik-forward-auth ./
|
|
||||||
ENTRYPOINT ["./traefik-forward-auth"]
|
|
@ -1,18 +0,0 @@
|
|||||||
FROM golang:1.12-alpine as builder
|
|
||||||
|
|
||||||
# Setup
|
|
||||||
RUN mkdir -p /go/src/github.com/thomseddon/traefik-forward-auth
|
|
||||||
WORKDIR /go/src/github.com/thomseddon/traefik-forward-auth
|
|
||||||
|
|
||||||
# Add libraries
|
|
||||||
RUN apk add --no-cache git
|
|
||||||
|
|
||||||
# Copy & build
|
|
||||||
ADD . /go/src/github.com/thomseddon/traefik-forward-auth/
|
|
||||||
RUN CGO_ENABLED=0 GOOS=linux GOARCH=arm64 GO111MODULE=on go build -a -installsuffix nocgo -o /traefik-forward-auth github.com/thomseddon/traefik-forward-auth/cmd
|
|
||||||
|
|
||||||
# Copy into scratch container
|
|
||||||
FROM scratch
|
|
||||||
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
|
||||||
COPY --from=builder /traefik-forward-auth ./
|
|
||||||
ENTRYPOINT ["./traefik-forward-auth"]
|
|
@ -1,5 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) [2023] [Wolfgang Hottgenroth]
|
||||||
Copyright (c) [2018] [Thom Seddon]
|
Copyright (c) [2018] [Thom Seddon]
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
5
Makefile
5
Makefile
@ -2,4 +2,7 @@
|
|||||||
format:
|
format:
|
||||||
gofmt -w -s internal/*.go internal/provider/*.go cmd/*.go
|
gofmt -w -s internal/*.go internal/provider/*.go cmd/*.go
|
||||||
|
|
||||||
.PHONY: format
|
test:
|
||||||
|
go test -v ./...
|
||||||
|
|
||||||
|
.PHONY: format test
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
|
|
||||||
# Traefik Forward Auth [](https://travis-ci.org/thomseddon/traefik-forward-auth) [](https://goreportcard.com/report/github.com/thomseddon/traefik-forward-auth)  [](https://GitHub.com/thomseddon/traefik-forward-auth/releases/)
|
# Traefik Forward Auth  [](https://goreportcard.com/report/github.com/thomseddon/traefik-forward-auth)  [](https://GitHub.com/thomseddon/traefik-forward-auth/releases/)
|
||||||
|
|
||||||
|
|
||||||
A minimal forward authentication service that provides OAuth/SSO login and authentication for the [traefik](https://github.com/containous/traefik) reverse proxy/load balancer.
|
A minimal forward authentication service that provides OAuth/SSO login and authentication for the [traefik](https://github.com/containous/traefik) reverse proxy/load balancer.
|
||||||
@ -9,8 +9,8 @@ A minimal forward authentication service that provides OAuth/SSO login and authe
|
|||||||
- Seamlessly overlays any http service with a single endpoint (see: `url-path` in [Configuration](#configuration))
|
- Seamlessly overlays any http service with a single endpoint (see: `url-path` in [Configuration](#configuration))
|
||||||
- Supports multiple providers including Google and OpenID Connect (supported by Azure, Github, Salesforce etc.)
|
- Supports multiple providers including Google and OpenID Connect (supported by Azure, Github, Salesforce etc.)
|
||||||
- Supports multiple domains/subdomains by dynamically generating redirect_uri's
|
- Supports multiple domains/subdomains by dynamically generating redirect_uri's
|
||||||
- Allows authentication to be selectively applied/bypassed based on request parameters (see `rules` in [Configuration](#configuration)))
|
- Allows authentication to be selectively applied/bypassed based on request parameters (see `rules` in [Configuration](#configuration))
|
||||||
- Supports use of centralised authentication host/redirect_uri (see `auth-host` in [Configuration](#configuration)))
|
- Supports use of centralised authentication host/redirect_uri (see `auth-host` in [Configuration](#configuration))
|
||||||
- Allows authentication to persist across multiple domains (see [Cookie Domains](#cookie-domains))
|
- Allows authentication to persist across multiple domains (see [Cookie Domains](#cookie-domains))
|
||||||
- Supports extended authentication beyond Google token lifetime (see: `lifetime` in [Configuration](#configuration))
|
- Supports extended authentication beyond Google token lifetime (see: `lifetime` in [Configuration](#configuration))
|
||||||
|
|
||||||
@ -27,18 +27,28 @@ A minimal forward authentication service that provides OAuth/SSO login and authe
|
|||||||
- [Concepts](#concepts)
|
- [Concepts](#concepts)
|
||||||
- [Forwarded Headers](#forwarded-headers)
|
- [Forwarded Headers](#forwarded-headers)
|
||||||
- [User Restriction](#user-restriction)
|
- [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)
|
- [Operation Modes](#operation-modes)
|
||||||
- [Overlay Mode](#overlay-mode)
|
- [Overlay Mode](#overlay-mode)
|
||||||
- [Auth Host Mode](#auth-host-mode)
|
- [Auth Host Mode](#auth-host-mode)
|
||||||
|
- [Logging Out](#logging-out)
|
||||||
- [Copyright](#copyright)
|
- [Copyright](#copyright)
|
||||||
- [License](#license)
|
- [License](#license)
|
||||||
|
|
||||||
## Releases
|
## 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).
|
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`).
|
||||||
|
|
||||||
|
We also build binary files for usage without docker starting with releases after 2.2.0 You can find these as assets of the specific GitHub release.
|
||||||
|
|
||||||
#### Upgrade Guide
|
#### 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.
|
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 +66,11 @@ version: '3'
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
traefik:
|
traefik:
|
||||||
image: traefik:1.7
|
image: traefik:v2.2
|
||||||
|
command: --providers.docker
|
||||||
ports:
|
ports:
|
||||||
- "8085:80"
|
- "8085:80"
|
||||||
volumes:
|
volumes:
|
||||||
- ./traefik.toml:/traefik.toml
|
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
|
||||||
traefik-forward-auth:
|
traefik-forward-auth:
|
||||||
@ -70,37 +80,28 @@ services:
|
|||||||
- PROVIDERS_GOOGLE_CLIENT_SECRET=your-client-secret
|
- PROVIDERS_GOOGLE_CLIENT_SECRET=your-client-secret
|
||||||
- SECRET=something-random
|
- SECRET=something-random
|
||||||
- INSECURE_COOKIE=true # Example assumes no https, do not use in production
|
- 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:
|
whoami:
|
||||||
image: emilevauge/whoami:latest
|
image: containous/whoami
|
||||||
labels:
|
labels:
|
||||||
- "traefik.frontend.rule=Host:whoami.mycompany.com"
|
- "traefik.http.routers.whoami.rule=Host(`whoami.mycompany.com`)"
|
||||||
```
|
- "traefik.http.routers.whoami.middlewares=traefik-forward-auth"
|
||||||
|
|
||||||
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"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Advanced:
|
#### 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/master/examples/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
|
#### 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
|
##### Google
|
||||||
|
|
||||||
Head to https://console.developers.google.com and make sure you've switched to the correct email account.
|
Head to https://console.developers.google.com and make sure you've switched to the correct email account.
|
||||||
@ -117,6 +118,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.
|
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
|
## Configuration
|
||||||
|
|
||||||
### Overview
|
### Overview
|
||||||
@ -137,12 +157,14 @@ Application Options:
|
|||||||
--cookie-name= Cookie Name (default: _forward_auth) [$COOKIE_NAME]
|
--cookie-name= Cookie Name (default: _forward_auth) [$COOKIE_NAME]
|
||||||
--csrf-cookie-name= CSRF Cookie Name (default: _forward_auth_csrf) [$CSRF_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-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]
|
--domain= Only allow given email domains, can be set multiple times [$DOMAIN]
|
||||||
--lifetime= Lifetime in seconds (default: 43200) [$LIFETIME]
|
--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]
|
--url-path= Callback URL Path (default: /_oauth) [$URL_PATH]
|
||||||
--secret= Secret used for signing (required) [$SECRET]
|
--secret= Secret used for signing (required) [$SECRET]
|
||||||
--whitelist= Only allow given email addresses, can be set multiple times [$WHITELIST]
|
--whitelist= Only allow given email addresses, can be set multiple times [$WHITELIST]
|
||||||
|
--port= Port to listen on (default: 4181) [$PORT]
|
||||||
--rule.<name>.<param>= Rule definitions, param can be: "action", "rule" or "provider"
|
--rule.<name>.<param>= Rule definitions, param can be: "action", "rule" or "provider"
|
||||||
|
|
||||||
Google Provider:
|
Google Provider:
|
||||||
@ -154,6 +176,18 @@ OIDC Provider:
|
|||||||
--providers.oidc.issuer-url= Issuer URL [$PROVIDERS_OIDC_ISSUER_URL]
|
--providers.oidc.issuer-url= Issuer URL [$PROVIDERS_OIDC_ISSUER_URL]
|
||||||
--providers.oidc.client-id= Client ID [$PROVIDERS_OIDC_CLIENT_ID]
|
--providers.oidc.client-id= Client ID [$PROVIDERS_OIDC_CLIENT_ID]
|
||||||
--providers.oidc.client-secret= Client Secret [$PROVIDERS_OIDC_CLIENT_SECRET]
|
--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:
|
Help Options:
|
||||||
-h, --help Show this help message
|
-h, --help Show this help message
|
||||||
@ -247,6 +281,20 @@ All options can be supplied in any of the following ways, in the following prece
|
|||||||
|
|
||||||
Default: `43200` (12 hours)
|
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`
|
- `url-path`
|
||||||
|
|
||||||
Customise the path that this service uses to handle the callback following authentication.
|
Customise the path that this service uses to handle the callback following authentication.
|
||||||
@ -276,6 +324,7 @@ All options can be supplied in any of the following ways, in the following prece
|
|||||||
- `action` - same usage as [`default-action`](#default-action), supported values:
|
- `action` - same usage as [`default-action`](#default-action), supported values:
|
||||||
- `auth` (default)
|
- `auth` (default)
|
||||||
- `allow`
|
- `allow`
|
||||||
|
- `domains` - optional, same usage as [`domain`](#domain)
|
||||||
- `provider` - same usage as [`default-provider`](#default-provider), supported values:
|
- `provider` - same usage as [`default-provider`](#default-provider), supported values:
|
||||||
- `google`
|
- `google`
|
||||||
- `oidc`
|
- `oidc`
|
||||||
@ -288,6 +337,7 @@ All options can be supplied in any of the following ways, in the following prece
|
|||||||
- ``Path(`path`, `/articles/{category}/{id:[0-9]+}`, ...)``
|
- ``Path(`path`, `/articles/{category}/{id:[0-9]+}`, ...)``
|
||||||
- ``PathPrefix(`/products/`, `/articles/{category}/{id:[0-9]+}`)``
|
- ``PathPrefix(`/products/`, `/articles/{category}/{id:[0-9]+}`)``
|
||||||
- ``Query(`foo=bar`, `bar=baz`)``
|
- ``Query(`foo=bar`, `bar=baz`)``
|
||||||
|
- `whitelist` - optional, same usage as whitelist`](#whitelist)
|
||||||
|
|
||||||
For example:
|
For example:
|
||||||
```
|
```
|
||||||
@ -303,8 +353,15 @@ All options can be supplied in any of the following ways, in the following prece
|
|||||||
rule.oidc.action = auth
|
rule.oidc.action = auth
|
||||||
rule.oidc.provider = oidc
|
rule.oidc.provider = oidc
|
||||||
rule.oidc.rule = PathPrefix(`/github`)
|
rule.oidc.rule = PathPrefix(`/github`)
|
||||||
|
|
||||||
|
# Allow jane@example.com to `/janes-eyes-only`
|
||||||
|
rule.two.action = allow
|
||||||
|
rule.two.rule = Path(`/janes-eyes-only`)
|
||||||
|
rule.two.whitelist = jane@example.com
|
||||||
```
|
```
|
||||||
|
|
||||||
|
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
|
## Concepts
|
||||||
|
|
||||||
### User Restriction
|
### User Restriction
|
||||||
@ -314,11 +371,90 @@ 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
|
* `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
|
* `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). If you set `domains` or `whitelist` on a rule, the global configuration is ignored.
|
||||||
|
|
||||||
### Forwarded Headers
|
### 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
|
||||||
|
```
|
||||||
|
|
||||||
|
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"
|
||||||
|
```
|
||||||
|
|
||||||
|
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
|
### Operation Modes
|
||||||
|
|
||||||
@ -339,7 +475,7 @@ As the hostname in the `redirect_uri` is dynamically generated based on the orig
|
|||||||
|
|
||||||
#### Auth Host Mode
|
#### 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.
|
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`.
|
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 +496,15 @@ Two criteria must be met for an `auth-host` to be used:
|
|||||||
1. Request matches given `cookie-domain`
|
1. Request matches given `cookie-domain`
|
||||||
2. `auth-host` is also subdomain of same `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
|
## Copyright
|
||||||
|
|
@ -1,6 +1,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
internal "github.com/thomseddon/traefik-forward-auth/internal"
|
internal "github.com/thomseddon/traefik-forward-auth/internal"
|
||||||
@ -24,7 +25,8 @@ func main() {
|
|||||||
http.HandleFunc("/", server.RootHandler)
|
http.HandleFunc("/", server.RootHandler)
|
||||||
|
|
||||||
// Start
|
// Start
|
||||||
log.Debugf("Starting with options: %s", config)
|
log.Info("wn test01 variant")
|
||||||
log.Info("Listening on :4181")
|
log.WithField("config", config).Debug("Starting with config")
|
||||||
log.Info(http.ListenAndServe(":4181", nil))
|
log.Infof("Listening on :%d", config.Port)
|
||||||
|
log.Info(http.ListenAndServe(fmt.Sprintf(":%d", config.Port), nil))
|
||||||
}
|
}
|
||||||
|
@ -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:
|
|
10
examples/traefik-v1.7/kubernetes/README.md
Normal file
10
examples/traefik-v1.7/kubernetes/README.md
Normal 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.
|
@ -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
|
||||||
|
```
|
@ -0,0 +1,3 @@
|
|||||||
|
bases:
|
||||||
|
- traefik-forward-auth
|
||||||
|
- whoami
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -0,0 +1,3 @@
|
|||||||
|
google-client-id=client-id
|
||||||
|
google-client-secret=client-secret
|
||||||
|
secret=something-random
|
@ -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")
|
@ -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
|
@ -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
|
@ -0,0 +1,7 @@
|
|||||||
|
commonLabels:
|
||||||
|
app: whoami
|
||||||
|
|
||||||
|
resources:
|
||||||
|
- deployment.yaml
|
||||||
|
- service.yaml
|
||||||
|
- ingress.yaml
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
```
|
@ -0,0 +1,3 @@
|
|||||||
|
bases:
|
||||||
|
- traefik
|
||||||
|
- whoami
|
@ -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")
|
@ -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"
|
@ -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
|
@ -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
|
@ -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
|
@ -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"
|
@ -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
|
@ -0,0 +1,3 @@
|
|||||||
|
google-client-id=client-id
|
||||||
|
google-client-secret=client-secret
|
||||||
|
secret=something-random
|
@ -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
|
@ -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
|
@ -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
|
@ -0,0 +1,7 @@
|
|||||||
|
commonLabels:
|
||||||
|
app: whoami
|
||||||
|
|
||||||
|
resources:
|
||||||
|
- deployment.yaml
|
||||||
|
- service.yaml
|
||||||
|
- ingress.yaml
|
@ -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
|
||||||
|
|
@ -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.
|
@ -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
|
@ -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
|
@ -14,7 +14,7 @@ services:
|
|||||||
- /var/run/docker.sock:/var/run/docker.sock
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
|
||||||
whoami1:
|
whoami1:
|
||||||
image: emilevauge/whoami
|
image: containous/whoami
|
||||||
networks:
|
networks:
|
||||||
- traefik
|
- traefik
|
||||||
labels:
|
labels:
|
||||||
@ -23,7 +23,7 @@ services:
|
|||||||
- "traefik.frontend.rule=Host:whoami.yourdomain.com"
|
- "traefik.frontend.rule=Host:whoami.yourdomain.com"
|
||||||
|
|
||||||
traefik-forward-auth:
|
traefik-forward-auth:
|
||||||
image: thomseddon/traefik-forward-auth
|
image: thomseddon/traefik-forward-auth:2
|
||||||
environment:
|
environment:
|
||||||
- PROVIDERS_GOOGLE_CLIENT_ID=your-client-id
|
- PROVIDERS_GOOGLE_CLIENT_ID=your-client-id
|
||||||
- PROVIDERS_GOOGLE_CLIENT_SECRET=your-client-secret
|
- PROVIDERS_GOOGLE_CLIENT_SECRET=your-client-secret
|
@ -14,7 +14,7 @@ services:
|
|||||||
- /var/run/docker.sock:/var/run/docker.sock
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
|
||||||
whoami1:
|
whoami1:
|
||||||
image: emilevauge/whoami
|
image: containous/whoami
|
||||||
networks:
|
networks:
|
||||||
- traefik
|
- traefik
|
||||||
labels:
|
labels:
|
||||||
@ -23,7 +23,7 @@ services:
|
|||||||
- "traefik.frontend.rule=Host:whoami.localhost.com"
|
- "traefik.frontend.rule=Host:whoami.localhost.com"
|
||||||
|
|
||||||
traefik-forward-auth:
|
traefik-forward-auth:
|
||||||
build: ../
|
build: thomseddon/traefik-forward-auth:2
|
||||||
environment:
|
environment:
|
||||||
- DEFAULT_PROVIDER=oidc
|
- DEFAULT_PROVIDER=oidc
|
||||||
- PROVIDERS_OIDC_ISSUER_URL=https://login.microsoftonline.com/{tenant}
|
- PROVIDERS_OIDC_ISSUER_URL=https://login.microsoftonline.com/{tenant}
|
@ -14,7 +14,7 @@ services:
|
|||||||
- /var/run/docker.sock:/var/run/docker.sock
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
|
||||||
whoami1:
|
whoami1:
|
||||||
image: emilevauge/whoami
|
image: containous/whoami
|
||||||
networks:
|
networks:
|
||||||
- traefik
|
- traefik
|
||||||
labels:
|
labels:
|
||||||
@ -23,8 +23,8 @@ services:
|
|||||||
- "traefik.frontend.rule=Host:whoami.localhost.com"
|
- "traefik.frontend.rule=Host:whoami.localhost.com"
|
||||||
|
|
||||||
traefik-forward-auth:
|
traefik-forward-auth:
|
||||||
build: ../
|
build: thomseddon/traefik-forward-auth:2
|
||||||
command: ./traefik-forward-auth --rule.1.action=allow --rule.1.rule="Path(`/`)"
|
command: ./traefik-forward-auth --rule.1.action=allow --rule.1.rule="Path(`/public`)"
|
||||||
environment:
|
environment:
|
||||||
- PROVIDERS_GOOGLE_CLIENT_ID=your-client-id
|
- PROVIDERS_GOOGLE_CLIENT_ID=your-client-id
|
||||||
- PROVIDERS_GOOGLE_CLIENT_SECRET=your-client-secret
|
- PROVIDERS_GOOGLE_CLIENT_SECRET=your-client-secret
|
@ -135,3 +135,4 @@
|
|||||||
# Enable Docker configuration backend
|
# Enable Docker configuration backend
|
||||||
[docker]
|
[docker]
|
||||||
exposedByDefault = false
|
exposedByDefault = false
|
||||||
|
network = "traefik"
|
@ -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
|
||||||
|
```
|
@ -0,0 +1,3 @@
|
|||||||
|
bases:
|
||||||
|
- traefik-forward-auth
|
||||||
|
- whoami
|
@ -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")
|
@ -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
|
@ -0,0 +1,20 @@
|
|||||||
|
#
|
||||||
|
# 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
|
@ -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
|
@ -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
|
@ -0,0 +1,3 @@
|
|||||||
|
google-client-id=client-id
|
||||||
|
google-client-secret=client-secret
|
||||||
|
secret=something-random
|
@ -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
|
@ -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
|
@ -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
|
@ -0,0 +1,7 @@
|
|||||||
|
commonLabels:
|
||||||
|
app: whoami
|
||||||
|
|
||||||
|
resources:
|
||||||
|
- deployment.yaml
|
||||||
|
- service.yaml
|
||||||
|
- ingress.yaml
|
@ -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
|
||||||
|
|
18
examples/traefik-v2/kubernetes/advanced-single-pod/README.md
Normal file
18
examples/traefik-v2/kubernetes/advanced-single-pod/README.md
Normal 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
|
||||||
|
```
|
@ -0,0 +1,3 @@
|
|||||||
|
bases:
|
||||||
|
- traefik
|
||||||
|
- whoami
|
@ -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")
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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"
|
@ -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
|
@ -0,0 +1,3 @@
|
|||||||
|
google-client-id=client-id
|
||||||
|
google-client-secret=client-secret
|
||||||
|
secret=something-random
|
@ -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
|
@ -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
|
@ -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
|
@ -0,0 +1,7 @@
|
|||||||
|
commonLabels:
|
||||||
|
app: whoami
|
||||||
|
|
||||||
|
resources:
|
||||||
|
- deployment.yaml
|
||||||
|
- service.yaml
|
||||||
|
- ingress.yaml
|
@ -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
|
||||||
|
|
39
examples/traefik-v2/kubernetes/simple-separate-pod/README.md
Normal file
39
examples/traefik-v2/kubernetes/simple-separate-pod/README.md
Normal 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.
|
@ -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
|
@ -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
|
37
examples/traefik-v2/swarm/docker-compose-auth-host.yml
Normal file
37
examples/traefik-v2/swarm/docker-compose-auth-host.yml
Normal 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"
|
34
examples/traefik-v2/swarm/docker-compose-oidc.yml
Normal file
34
examples/traefik-v2/swarm/docker-compose-oidc.yml
Normal 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"
|
||||||
|
|
32
examples/traefik-v2/swarm/docker-compose.yml
Normal file
32
examples/traefik-v2/swarm/docker-compose.yml
Normal 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"
|
15
go.sum
15
go.sum
@ -127,7 +127,6 @@ github.com/go-acme/lego/v3 v3.2.0/go.mod h1:074uqt+JS6plx+c9Xaiz6+L+GBb+7itGtzfc
|
|||||||
github.com/go-cmd/cmd v1.0.5/go.mod h1:y8q8qlK5wQibcw63djSl/ntiHUHXHGdCkPk0j4QeW4s=
|
github.com/go-cmd/cmd v1.0.5/go.mod h1:y8q8qlK5wQibcw63djSl/ntiHUHXHGdCkPk0j4QeW4s=
|
||||||
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
|
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
|
||||||
github.com/go-ini/ini v1.44.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
|
github.com/go-ini/ini v1.44.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
|
||||||
github.com/go-kit/kit v0.8.0 h1:Wz+5lgoB0kkuqLEc6NVmwRknTKP6dTGbSqvhZtBI/j0=
|
|
||||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||||
github.com/go-kit/kit v0.9.0 h1:wDJmvq38kDhkVxi50ni9ykkdUr1PKgqKOoi01fa0Mdk=
|
github.com/go-kit/kit v0.9.0 h1:wDJmvq38kDhkVxi50ni9ykkdUr1PKgqKOoi01fa0Mdk=
|
||||||
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||||
@ -145,7 +144,6 @@ github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4er
|
|||||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||||
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
|
|
||||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
|
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
|
||||||
@ -229,7 +227,6 @@ github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfV
|
|||||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||||
github.com/kolo/xmlrpc v0.0.0-20190717152603-07c4ee3fd181/go.mod h1:o03bZfuBwAXHetKXuInt4S7omeXUu62/A845kiycsSQ=
|
github.com/kolo/xmlrpc v0.0.0-20190717152603-07c4ee3fd181/go.mod h1:o03bZfuBwAXHetKXuInt4S7omeXUu62/A845kiycsSQ=
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
|
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
|
github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
@ -349,7 +346,6 @@ github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdh
|
|||||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
||||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||||
github.com/sirupsen/logrus v1.4.1 h1:GL2rEmy6nsikmW0r8opw9JIRScdMF5hA8cOYLH7In1k=
|
|
||||||
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
|
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
|
||||||
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
|
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
|
||||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||||
@ -357,13 +353,11 @@ github.com/skratchdot/open-golang v0.0.0-20160302144031-75fb7ed4208c/go.mod h1:s
|
|||||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||||
github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||||
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
|
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
|
||||||
github.com/spf13/pflag v1.0.1 h1:aCvUg6QPl3ibpQUxyLkrEkCHtPqYJL4x9AuhqVqFis4=
|
|
||||||
github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||||
github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
|
github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
@ -436,7 +430,6 @@ golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73r
|
|||||||
golang.org/x/net v0.0.0-20190206173232-65e2d4e15006/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20190206173232-65e2d4e15006/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ=
|
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
@ -457,7 +450,6 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
|
|||||||
golang.org/x/sys v0.0.0-20180622082034-63fc586f45fe/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180622082034-63fc586f45fe/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33 h1:I6FyU15t786LL7oL/hn43zqTuEGr4PN7F4XJ1p4E3Y8=
|
|
||||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
@ -469,7 +461,6 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h
|
|||||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc=
|
|
||||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
@ -493,10 +484,8 @@ golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3
|
|||||||
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135 h1:5Beo0mZN8dRzgrMMkDp0jc8YXQKx9DiJ2k1dkvGsn5A=
|
|
||||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
gonum.org/v1/gonum v0.0.0-20190331200053-3d26580ed485 h1:OB/uP/Puiu5vS5QMRPrXCDWUPb+kt8f1KW8oQzFejQw=
|
|
||||||
gonum.org/v1/gonum v0.0.0-20190331200053-3d26580ed485/go.mod h1:2ltnJ7xHfj0zHS40VVPYEAAMTa3ZGguvHGBSJeRWqE0=
|
gonum.org/v1/gonum v0.0.0-20190331200053-3d26580ed485/go.mod h1:2ltnJ7xHfj0zHS40VVPYEAAMTa3ZGguvHGBSJeRWqE0=
|
||||||
gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=
|
gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=
|
||||||
gonum.org/v1/netlib v0.0.0-20190331212654-76723241ea4e/go.mod h1:kS+toOQn6AQKjmKJ7gzohV1XkqsFehRA2FbsbkopSuQ=
|
gonum.org/v1/netlib v0.0.0-20190331212654-76723241ea4e/go.mod h1:kS+toOQn6AQKjmKJ7gzohV1XkqsFehRA2FbsbkopSuQ=
|
||||||
@ -504,7 +493,6 @@ google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMt
|
|||||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||||
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||||
google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
|
|
||||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||||
google.golang.org/appengine v1.5.0 h1:KxkO13IPW4Lslp2bz+KHP2E3gtFlrIGNThxkZQ3g+4c=
|
google.golang.org/appengine v1.5.0 h1:KxkO13IPW4Lslp2bz+KHP2E3gtFlrIGNThxkZQ3g+4c=
|
||||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||||
@ -553,11 +541,8 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
|
|||||||
k8s.io/api v0.0.0-20190718183219-b59d8169aab5/go.mod h1:TBhBqb1AWbBQbW3XRusr7n7E4v2+5ZY8r8sAMnyFC5A=
|
k8s.io/api v0.0.0-20190718183219-b59d8169aab5/go.mod h1:TBhBqb1AWbBQbW3XRusr7n7E4v2+5ZY8r8sAMnyFC5A=
|
||||||
k8s.io/apimachinery v0.0.0-20190612205821-1799e75a0719/go.mod h1:I4A+glKBHiTgiEjQiCCQfCAIcIMFGt291SmsvcrFzJA=
|
k8s.io/apimachinery v0.0.0-20190612205821-1799e75a0719/go.mod h1:I4A+glKBHiTgiEjQiCCQfCAIcIMFGt291SmsvcrFzJA=
|
||||||
k8s.io/client-go v0.0.0-20190718183610-8e956561bbf5/go.mod h1:ozblAqkW495yoAX60QZyxQBq5W0YixE9Ffn4F91RO0g=
|
k8s.io/client-go v0.0.0-20190718183610-8e956561bbf5/go.mod h1:ozblAqkW495yoAX60QZyxQBq5W0YixE9Ffn4F91RO0g=
|
||||||
k8s.io/code-generator v0.0.0-20190612205613-18da4a14b22b h1:p+PRuwXWwk5e+UYvicGiavEupapqM5NOxUl3y1GkD6c=
|
|
||||||
k8s.io/code-generator v0.0.0-20190612205613-18da4a14b22b/go.mod h1:G8bQwmHm2eafm5bgtX67XDZQ8CWKSGu9DekI+yN4Y5I=
|
k8s.io/code-generator v0.0.0-20190612205613-18da4a14b22b/go.mod h1:G8bQwmHm2eafm5bgtX67XDZQ8CWKSGu9DekI+yN4Y5I=
|
||||||
k8s.io/gengo v0.0.0-20190116091435-f8a0810f38af h1:SwjZbO0u5ZuaV6TRMWOGB40iaycX8sbdMQHtjNZ19dk=
|
|
||||||
k8s.io/gengo v0.0.0-20190116091435-f8a0810f38af/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=
|
k8s.io/gengo v0.0.0-20190116091435-f8a0810f38af/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=
|
||||||
k8s.io/klog v0.3.1 h1:RVgyDHY/kFKtLqh67NvEWIgkMneNoIrdkN0CxDSQc68=
|
|
||||||
k8s.io/klog v0.3.1/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
|
k8s.io/klog v0.3.1/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
|
||||||
k8s.io/kube-openapi v0.0.0-20190228160746-b3a7cee44a30/go.mod h1:BXM9ceUBTj2QnfH2MK1odQs778ajze1RxcmP6S8RVVc=
|
k8s.io/kube-openapi v0.0.0-20190228160746-b3a7cee44a30/go.mod h1:BXM9ceUBTj2QnfH2MK1odQs778ajze1RxcmP6S8RVVc=
|
||||||
k8s.io/utils v0.0.0-20190221042446-c2654d5206da/go.mod h1:8k8uAuAQ0rXslZKaEWd0c3oVhZz7sSzSiPnVZayjIX0=
|
k8s.io/utils v0.0.0-20190221042446-c2654d5206da/go.mod h1:8k8uAuAQ0rXslZKaEWd0c3oVhZz7sSzSiPnVZayjIX0=
|
||||||
|
175
internal/auth.go
175
internal/auth.go
@ -17,6 +17,7 @@ import (
|
|||||||
|
|
||||||
// Request Validation
|
// Request Validation
|
||||||
|
|
||||||
|
// ValidateCookie verifies that a cookie matches the expected format of:
|
||||||
// Cookie = hash(secret, cookie domain, email, expires)|expires|email
|
// Cookie = hash(secret, cookie domain, email, expires)|expires|email
|
||||||
func ValidateCookie(r *http.Request, c *http.Cookie) (string, error) {
|
func ValidateCookie(r *http.Request, c *http.Cookie) (string, error) {
|
||||||
parts := strings.Split(c.Value, "|")
|
parts := strings.Split(c.Value, "|")
|
||||||
@ -55,54 +56,88 @@ func ValidateCookie(r *http.Request, c *http.Cookie) (string, error) {
|
|||||||
return parts[2], nil
|
return parts[2], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate email
|
// ValidateEmail checks if the given email address matches either a whitelisted
|
||||||
func ValidateEmail(email string) bool {
|
// email address, as defined by the "whitelist" config parameter. Or is part of
|
||||||
found := false
|
// a permitted domain, as defined by the "domains" config parameter
|
||||||
if len(config.Whitelist) > 0 {
|
func ValidateEmail(email, ruleName string) bool {
|
||||||
for _, whitelist := range config.Whitelist {
|
// Use global config by default
|
||||||
if email == whitelist {
|
whitelist := config.Whitelist
|
||||||
found = true
|
domains := config.Domains
|
||||||
}
|
|
||||||
|
if rule, ok := config.Rules[ruleName]; ok {
|
||||||
|
// Override with rule config if found
|
||||||
|
if len(rule.Whitelist) > 0 || len(rule.Domains) > 0 {
|
||||||
|
whitelist = rule.Whitelist
|
||||||
|
domains = rule.Domains
|
||||||
}
|
}
|
||||||
} else if len(config.Domains) > 0 {
|
}
|
||||||
parts := strings.Split(email, "@")
|
|
||||||
if len(parts) < 2 {
|
// Do we have any validation to perform?
|
||||||
return false
|
if len(whitelist) == 0 && len(domains) == 0 {
|
||||||
}
|
|
||||||
for _, domain := range config.Domains {
|
|
||||||
if domain == parts[1] {
|
|
||||||
found = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
return found
|
// Email whitelist validation
|
||||||
|
if len(whitelist) > 0 {
|
||||||
|
if ValidateWhitelist(email, whitelist) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we're not matching *either*, stop here
|
||||||
|
if !config.MatchWhitelistOrDomain {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Domain validation
|
||||||
|
if len(domains) > 0 && ValidateDomains(email, domains) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateWhitelist checks if the email is in whitelist
|
||||||
|
func ValidateWhitelist(email string, whitelist CommaSeparatedList) bool {
|
||||||
|
for _, whitelist := range whitelist {
|
||||||
|
if email == whitelist {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateDomains checks if the email matches a whitelisted domain
|
||||||
|
func ValidateDomains(email string, domains CommaSeparatedList) bool {
|
||||||
|
parts := strings.Split(email, "@")
|
||||||
|
if len(parts) < 2 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, domain := range domains {
|
||||||
|
if domain == parts[1] {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Utility methods
|
// Utility methods
|
||||||
|
|
||||||
// Get the redirect base
|
// Get the redirect base
|
||||||
func redirectBase(r *http.Request) string {
|
func redirectBase(r *http.Request) string {
|
||||||
proto := r.Header.Get("X-Forwarded-Proto")
|
return fmt.Sprintf("%s://%s", r.Header.Get("X-Forwarded-Proto"), r.Host)
|
||||||
host := r.Header.Get("X-Forwarded-Host")
|
|
||||||
|
|
||||||
return fmt.Sprintf("%s://%s", proto, host)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return url
|
// Return url
|
||||||
func returnUrl(r *http.Request) string {
|
func returnUrl(r *http.Request) string {
|
||||||
path := r.Header.Get("X-Forwarded-Uri")
|
return fmt.Sprintf("%s%s", redirectBase(r), r.URL.Path)
|
||||||
|
|
||||||
return fmt.Sprintf("%s%s", redirectBase(r), path)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get oauth redirect uri
|
// Get oauth redirect uri
|
||||||
func redirectUri(r *http.Request) string {
|
func redirectUri(r *http.Request) string {
|
||||||
if use, _ := useAuthDomain(r); use {
|
if use, _ := useAuthDomain(r); use {
|
||||||
proto := r.Header.Get("X-Forwarded-Proto")
|
p := r.Header.Get("X-Forwarded-Proto")
|
||||||
return fmt.Sprintf("%s://%s%s", proto, config.AuthHost, config.Path)
|
return fmt.Sprintf("%s://%s%s", p, config.AuthHost, config.Path)
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Sprintf("%s%s", redirectBase(r), config.Path)
|
return fmt.Sprintf("%s%s", redirectBase(r), config.Path)
|
||||||
@ -115,7 +150,7 @@ func useAuthDomain(r *http.Request) (bool, string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Does the request match a given cookie domain?
|
// Does the request match a given cookie domain?
|
||||||
reqMatch, reqHost := matchCookieDomains(r.Header.Get("X-Forwarded-Host"))
|
reqMatch, reqHost := matchCookieDomains(r.Host)
|
||||||
|
|
||||||
// Do any of the auth hosts match a cookie domain?
|
// Do any of the auth hosts match a cookie domain?
|
||||||
authMatch, authHost := matchCookieDomains(config.AuthHost)
|
authMatch, authHost := matchCookieDomains(config.AuthHost)
|
||||||
@ -126,7 +161,7 @@ func useAuthDomain(r *http.Request) (bool, string) {
|
|||||||
|
|
||||||
// Cookie methods
|
// Cookie methods
|
||||||
|
|
||||||
// Create an auth cookie
|
// MakeCookie creates an auth cookie
|
||||||
func MakeCookie(r *http.Request, email string) *http.Cookie {
|
func MakeCookie(r *http.Request, email string) *http.Cookie {
|
||||||
expires := cookieExpiry()
|
expires := cookieExpiry()
|
||||||
mac := cookieSignature(r, email, fmt.Sprintf("%d", expires.Unix()))
|
mac := cookieSignature(r, email, fmt.Sprintf("%d", expires.Unix()))
|
||||||
@ -143,23 +178,44 @@ 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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildCSRFCookieName(nonce string) string {
|
||||||
|
return config.CSRFCookieName + "_" + nonce[:6]
|
||||||
|
}
|
||||||
|
|
||||||
|
// MakeCSRFCookie makes a csrf cookie (used during login only)
|
||||||
|
//
|
||||||
|
// Note, CSRF cookies live shorter than auth cookies, a fixed 1h.
|
||||||
|
// That's because some CSRF cookies may belong to auth flows that don't complete
|
||||||
|
// and thus may not get cleared by ClearCookie.
|
||||||
func MakeCSRFCookie(r *http.Request, nonce string) *http.Cookie {
|
func MakeCSRFCookie(r *http.Request, nonce string) *http.Cookie {
|
||||||
return &http.Cookie{
|
return &http.Cookie{
|
||||||
Name: config.CSRFCookieName,
|
Name: buildCSRFCookieName(nonce),
|
||||||
Value: nonce,
|
Value: nonce,
|
||||||
Path: "/",
|
Path: "/",
|
||||||
Domain: csrfCookieDomain(r),
|
Domain: csrfCookieDomain(r),
|
||||||
HttpOnly: true,
|
HttpOnly: true,
|
||||||
Secure: !config.InsecureCookie,
|
Secure: !config.InsecureCookie,
|
||||||
Expires: cookieExpiry(),
|
Expires: time.Now().Local().Add(time.Hour * 1),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a cookie to clear csrf cookie
|
// ClearCSRFCookie makes an expired csrf cookie to clear csrf cookie
|
||||||
func ClearCSRFCookie(r *http.Request) *http.Cookie {
|
func ClearCSRFCookie(r *http.Request, c *http.Cookie) *http.Cookie {
|
||||||
return &http.Cookie{
|
return &http.Cookie{
|
||||||
Name: config.CSRFCookieName,
|
Name: c.Name,
|
||||||
Value: "",
|
Value: "",
|
||||||
Path: "/",
|
Path: "/",
|
||||||
Domain: csrfCookieDomain(r),
|
Domain: csrfCookieDomain(r),
|
||||||
@ -169,18 +225,18 @@ func ClearCSRFCookie(r *http.Request) *http.Cookie {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate the csrf cookie against state
|
// FindCSRFCookie extracts the CSRF cookie from the request based on state.
|
||||||
func ValidateCSRFCookie(r *http.Request, c *http.Cookie) (valid bool, provider string, redirect string, err error) {
|
func FindCSRFCookie(r *http.Request, state string) (c *http.Cookie, err error) {
|
||||||
state := r.URL.Query().Get("state")
|
// Check for CSRF cookie
|
||||||
|
return r.Cookie(buildCSRFCookieName(state))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateCSRFCookie validates the csrf cookie against state
|
||||||
|
func ValidateCSRFCookie(c *http.Cookie, state string) (valid bool, provider string, redirect string, err error) {
|
||||||
if len(c.Value) != 32 {
|
if len(c.Value) != 32 {
|
||||||
return false, "", "", errors.New("Invalid CSRF cookie value")
|
return false, "", "", errors.New("Invalid CSRF cookie value")
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(state) < 34 {
|
|
||||||
return false, "", "", errors.New("Invalid CSRF state value")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check nonce match
|
// Check nonce match
|
||||||
if c.Value != state[:32] {
|
if c.Value != state[:32] {
|
||||||
return false, "", "", errors.New("CSRF cookie does not match state")
|
return false, "", "", errors.New("CSRF cookie does not match state")
|
||||||
@ -197,12 +253,21 @@ func ValidateCSRFCookie(r *http.Request, c *http.Cookie) (valid bool, provider s
|
|||||||
return true, params[:split], params[split+1:], nil
|
return true, params[:split], params[split+1:], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MakeState generates a state value
|
||||||
func MakeState(r *http.Request, p provider.Provider, nonce string) string {
|
func MakeState(r *http.Request, p provider.Provider, nonce string) string {
|
||||||
return fmt.Sprintf("%s:%s:%s", nonce, p.Name(), returnUrl(r))
|
return fmt.Sprintf("%s:%s:%s", nonce, p.Name(), returnUrl(r))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ValidateState checks whether the state is of right length.
|
||||||
|
func ValidateState(state string) error {
|
||||||
|
if len(state) < 34 {
|
||||||
|
return errors.New("Invalid CSRF state value")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nonce generates a random nonce
|
||||||
func Nonce() (error, string) {
|
func Nonce() (error, string) {
|
||||||
// Make nonce
|
|
||||||
nonce := make([]byte, 16)
|
nonce := make([]byte, 16)
|
||||||
_, err := rand.Read(nonce)
|
_, err := rand.Read(nonce)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -214,10 +279,8 @@ func Nonce() (error, string) {
|
|||||||
|
|
||||||
// Cookie domain
|
// Cookie domain
|
||||||
func cookieDomain(r *http.Request) string {
|
func cookieDomain(r *http.Request) string {
|
||||||
host := r.Header.Get("X-Forwarded-Host")
|
|
||||||
|
|
||||||
// Check if any of the given cookie domains matches
|
// Check if any of the given cookie domains matches
|
||||||
_, domain := matchCookieDomains(host)
|
_, domain := matchCookieDomains(r.Host)
|
||||||
return domain
|
return domain
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -227,7 +290,7 @@ func csrfCookieDomain(r *http.Request) string {
|
|||||||
if use, domain := useAuthDomain(r); use {
|
if use, domain := useAuthDomain(r); use {
|
||||||
host = domain
|
host = domain
|
||||||
} else {
|
} else {
|
||||||
host = r.Header.Get("X-Forwarded-Host")
|
host = r.Host
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove port
|
// Remove port
|
||||||
@ -263,9 +326,7 @@ func cookieExpiry() time.Time {
|
|||||||
return time.Now().Local().Add(config.Lifetime)
|
return time.Now().Local().Add(config.Lifetime)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cookie Domain
|
// CookieDomain holds cookie domain info
|
||||||
|
|
||||||
// Cookie Domain
|
|
||||||
type CookieDomain struct {
|
type CookieDomain struct {
|
||||||
Domain string
|
Domain string
|
||||||
DomainLen int
|
DomainLen int
|
||||||
@ -273,6 +334,7 @@ type CookieDomain struct {
|
|||||||
SubDomainLen int
|
SubDomainLen int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewCookieDomain creates a new CookieDomain from the given domain string
|
||||||
func NewCookieDomain(domain string) *CookieDomain {
|
func NewCookieDomain(domain string) *CookieDomain {
|
||||||
return &CookieDomain{
|
return &CookieDomain{
|
||||||
Domain: domain,
|
Domain: domain,
|
||||||
@ -282,6 +344,7 @@ func NewCookieDomain(domain string) *CookieDomain {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Match checks if the given host matches this CookieDomain
|
||||||
func (c *CookieDomain) Match(host string) bool {
|
func (c *CookieDomain) Match(host string) bool {
|
||||||
// Exact domain match?
|
// Exact domain match?
|
||||||
if host == c.Domain {
|
if host == c.Domain {
|
||||||
@ -296,19 +359,22 @@ func (c *CookieDomain) Match(host string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UnmarshalFlag converts a string to a CookieDomain
|
||||||
func (c *CookieDomain) UnmarshalFlag(value string) error {
|
func (c *CookieDomain) UnmarshalFlag(value string) error {
|
||||||
*c = *NewCookieDomain(value)
|
*c = *NewCookieDomain(value)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MarshalFlag converts a CookieDomain to a string
|
||||||
func (c *CookieDomain) MarshalFlag() (string, error) {
|
func (c *CookieDomain) MarshalFlag() (string, error) {
|
||||||
return c.Domain, nil
|
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
|
type CookieDomains []CookieDomain
|
||||||
|
|
||||||
|
// UnmarshalFlag converts a comma separated list of cookie domains to an array
|
||||||
|
// of CookieDomains
|
||||||
func (c *CookieDomains) UnmarshalFlag(value string) error {
|
func (c *CookieDomains) UnmarshalFlag(value string) error {
|
||||||
if len(value) > 0 {
|
if len(value) > 0 {
|
||||||
for _, d := range strings.Split(value, ",") {
|
for _, d := range strings.Split(value, ",") {
|
||||||
@ -319,6 +385,7 @@ func (c *CookieDomains) UnmarshalFlag(value string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MarshalFlag converts an array of CookieDomain to a comma seperated list
|
||||||
func (c *CookieDomains) MarshalFlag() (string, error) {
|
func (c *CookieDomains) MarshalFlag() (string, error) {
|
||||||
var domains []string
|
var domains []string
|
||||||
for _, d := range *c {
|
for _, d := range *c {
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
package tfa
|
package tfa
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
@ -66,42 +66,139 @@ func TestAuthValidateEmail(t *testing.T) {
|
|||||||
assert := assert.New(t)
|
assert := assert.New(t)
|
||||||
config, _ = NewConfig([]string{})
|
config, _ = NewConfig([]string{})
|
||||||
|
|
||||||
// Should allow any
|
// Should allow any with no whitelist/domain is specified
|
||||||
v := ValidateEmail("test@test.com")
|
v := ValidateEmail("test@test.com", "default")
|
||||||
assert.True(v, "should allow any domain if email domain is not defined")
|
assert.True(v, "should allow any domain if email domain is not defined")
|
||||||
v = ValidateEmail("one@two.com")
|
v = ValidateEmail("one@two.com", "default")
|
||||||
assert.True(v, "should allow any domain if email domain is not defined")
|
assert.True(v, "should allow any domain if email domain is not defined")
|
||||||
|
|
||||||
// Should block non matching domain
|
|
||||||
config.Domains = []string{"test.com"}
|
|
||||||
v = ValidateEmail("one@two.com")
|
|
||||||
assert.False(v, "should not allow user from another domain")
|
|
||||||
|
|
||||||
// Should allow matching domain
|
// Should allow matching domain
|
||||||
config.Domains = []string{"test.com"}
|
config.Domains = []string{"test.com"}
|
||||||
v = ValidateEmail("test@test.com")
|
v = ValidateEmail("one@two.com", "default")
|
||||||
|
assert.False(v, "should not allow user from another domain")
|
||||||
|
v = ValidateEmail("test@test.com", "default")
|
||||||
assert.True(v, "should allow user from allowed domain")
|
assert.True(v, "should allow user from allowed domain")
|
||||||
|
|
||||||
// Should block non whitelisted email address
|
|
||||||
config.Domains = []string{}
|
|
||||||
config.Whitelist = []string{"test@test.com"}
|
|
||||||
v = ValidateEmail("one@two.com")
|
|
||||||
assert.False(v, "should not allow user not in whitelist")
|
|
||||||
|
|
||||||
// Should allow matching whitelisted email address
|
// Should allow matching whitelisted email address
|
||||||
config.Domains = []string{}
|
config.Domains = []string{}
|
||||||
config.Whitelist = []string{"test@test.com"}
|
config.Whitelist = []string{"test@test.com"}
|
||||||
v = ValidateEmail("test@test.com")
|
v = ValidateEmail("one@two.com", "default")
|
||||||
|
assert.False(v, "should not allow user not in whitelist")
|
||||||
|
v = ValidateEmail("test@test.com", "default")
|
||||||
|
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("one@two.com", "default")
|
||||||
|
assert.False(v, "should not allow user not in either")
|
||||||
|
v = ValidateEmail("test@example.com", "default")
|
||||||
|
assert.False(v, "should not allow user from allowed domain")
|
||||||
|
v = ValidateEmail("test@test.com", "default")
|
||||||
|
assert.True(v, "should allow user in whitelist")
|
||||||
|
|
||||||
|
// 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("one@two.com", "default")
|
||||||
|
assert.False(v, "should not allow user not in either")
|
||||||
|
v = ValidateEmail("test@example.com", "default")
|
||||||
|
assert.True(v, "should allow user from allowed domain")
|
||||||
|
v = ValidateEmail("test@test.com", "default")
|
||||||
|
assert.True(v, "should allow user in whitelist")
|
||||||
|
|
||||||
|
// Rule testing
|
||||||
|
|
||||||
|
// Should use global whitelist/domain when not specified on rule
|
||||||
|
config.Domains = []string{"example.com"}
|
||||||
|
config.Whitelist = []string{"test@test.com"}
|
||||||
|
config.Rules = map[string]*Rule{"test": NewRule()}
|
||||||
|
config.MatchWhitelistOrDomain = true
|
||||||
|
v = ValidateEmail("one@two.com", "test")
|
||||||
|
assert.False(v, "should not allow user not in either")
|
||||||
|
v = ValidateEmail("test@example.com", "test")
|
||||||
|
assert.True(v, "should allow user from allowed global domain")
|
||||||
|
v = ValidateEmail("test@test.com", "test")
|
||||||
|
assert.True(v, "should allow user in global whitelist")
|
||||||
|
|
||||||
|
// Should allow matching domain in rule
|
||||||
|
config.Domains = []string{"testglobal.com"}
|
||||||
|
config.Whitelist = []string{}
|
||||||
|
rule := NewRule()
|
||||||
|
config.Rules = map[string]*Rule{"test": rule}
|
||||||
|
rule.Domains = []string{"testrule.com"}
|
||||||
|
config.MatchWhitelistOrDomain = false
|
||||||
|
v = ValidateEmail("one@two.com", "test")
|
||||||
|
assert.False(v, "should not allow user from another domain")
|
||||||
|
v = ValidateEmail("one@testglobal.com", "test")
|
||||||
|
assert.False(v, "should not allow user from global domain")
|
||||||
|
v = ValidateEmail("test@testrule.com", "test")
|
||||||
|
assert.True(v, "should allow user from allowed domain")
|
||||||
|
|
||||||
|
// Should allow matching whitelist in rule
|
||||||
|
config.Domains = []string{}
|
||||||
|
config.Whitelist = []string{"test@testglobal.com"}
|
||||||
|
rule = NewRule()
|
||||||
|
config.Rules = map[string]*Rule{"test": rule}
|
||||||
|
rule.Whitelist = []string{"test@testrule.com"}
|
||||||
|
config.MatchWhitelistOrDomain = false
|
||||||
|
v = ValidateEmail("one@two.com", "test")
|
||||||
|
assert.False(v, "should not allow user from another domain")
|
||||||
|
v = ValidateEmail("test@testglobal.com", "test")
|
||||||
|
assert.False(v, "should not allow user from global domain")
|
||||||
|
v = ValidateEmail("test@testrule.com", "test")
|
||||||
|
assert.True(v, "should allow user from allowed domain")
|
||||||
|
|
||||||
|
// Should allow only matching email address when
|
||||||
|
// MatchWhitelistOrDomain is disabled
|
||||||
|
config.Domains = []string{"exampleglobal.com"}
|
||||||
|
config.Whitelist = []string{"test@testglobal.com"}
|
||||||
|
rule = NewRule()
|
||||||
|
config.Rules = map[string]*Rule{"test": rule}
|
||||||
|
rule.Domains = []string{"examplerule.com"}
|
||||||
|
rule.Whitelist = []string{"test@testrule.com"}
|
||||||
|
config.MatchWhitelistOrDomain = false
|
||||||
|
v = ValidateEmail("one@two.com", "test")
|
||||||
|
assert.False(v, "should not allow user not in either")
|
||||||
|
v = ValidateEmail("test@testglobal.com", "test")
|
||||||
|
assert.False(v, "should not allow user in global whitelist")
|
||||||
|
v = ValidateEmail("test@exampleglobal.com", "test")
|
||||||
|
assert.False(v, "should not allow user from global domain")
|
||||||
|
v = ValidateEmail("test@examplerule.com", "test")
|
||||||
|
assert.False(v, "should not allow user from allowed domain")
|
||||||
|
v = ValidateEmail("test@testrule.com", "test")
|
||||||
|
assert.True(v, "should allow user in whitelist")
|
||||||
|
|
||||||
|
// Should allow either matching domain or email address when
|
||||||
|
// MatchWhitelistOrDomain is enabled
|
||||||
|
config.Domains = []string{"exampleglobal.com"}
|
||||||
|
config.Whitelist = []string{"test@testglobal.com"}
|
||||||
|
rule = NewRule()
|
||||||
|
config.Rules = map[string]*Rule{"test": rule}
|
||||||
|
rule.Domains = []string{"examplerule.com"}
|
||||||
|
rule.Whitelist = []string{"test@testrule.com"}
|
||||||
|
config.MatchWhitelistOrDomain = true
|
||||||
|
v = ValidateEmail("one@two.com", "test")
|
||||||
|
assert.False(v, "should not allow user not in either")
|
||||||
|
v = ValidateEmail("test@testglobal.com", "test")
|
||||||
|
assert.False(v, "should not allow user in global whitelist")
|
||||||
|
v = ValidateEmail("test@exampleglobal.com", "test")
|
||||||
|
assert.False(v, "should not allow user from global domain")
|
||||||
|
v = ValidateEmail("test@examplerule.com", "test")
|
||||||
|
assert.True(v, "should allow user from allowed domain")
|
||||||
|
v = ValidateEmail("test@testrule.com", "test")
|
||||||
assert.True(v, "should allow user in whitelist")
|
assert.True(v, "should allow user in whitelist")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRedirectUri(t *testing.T) {
|
func TestRedirectUri(t *testing.T) {
|
||||||
assert := assert.New(t)
|
assert := assert.New(t)
|
||||||
|
|
||||||
r, _ := http.NewRequest("GET", "http://example.com", nil)
|
r := httptest.NewRequest("GET", "http://app.example.com/hello", nil)
|
||||||
r.Header.Add("X-Forwarded-Proto", "http")
|
r.Header.Add("X-Forwarded-Proto", "http")
|
||||||
r.Header.Add("X-Forwarded-Host", "app.example.com")
|
|
||||||
r.Header.Add("X-Forwarded-Uri", "/hello")
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// No Auth Host
|
// No Auth Host
|
||||||
@ -143,10 +240,8 @@ func TestRedirectUri(t *testing.T) {
|
|||||||
// With Auth URL + cookie domain, but from different domain
|
// With Auth URL + cookie domain, but from different domain
|
||||||
// - will not use auth host
|
// - will not use auth host
|
||||||
//
|
//
|
||||||
r, _ = http.NewRequest("GET", "http://another.com", nil)
|
r = httptest.NewRequest("GET", "https://another.com/hello", nil)
|
||||||
r.Header.Add("X-Forwarded-Proto", "https")
|
r.Header.Add("X-Forwarded-Proto", "https")
|
||||||
r.Header.Add("X-Forwarded-Host", "another.com")
|
|
||||||
r.Header.Add("X-Forwarded-Uri", "/hello")
|
|
||||||
|
|
||||||
config.AuthHost = "auth.example.com"
|
config.AuthHost = "auth.example.com"
|
||||||
config.CookieDomains = []CookieDomain{*NewCookieDomain("example.com")}
|
config.CookieDomains = []CookieDomain{*NewCookieDomain("example.com")}
|
||||||
@ -193,29 +288,30 @@ func TestAuthMakeCSRFCookie(t *testing.T) {
|
|||||||
|
|
||||||
// No cookie domain or auth url
|
// No cookie domain or auth url
|
||||||
c := MakeCSRFCookie(r, "12345678901234567890123456789012")
|
c := MakeCSRFCookie(r, "12345678901234567890123456789012")
|
||||||
|
assert.Equal("_forward_auth_csrf_123456", c.Name)
|
||||||
assert.Equal("app.example.com", c.Domain)
|
assert.Equal("app.example.com", c.Domain)
|
||||||
|
|
||||||
// With cookie domain but no auth url
|
// With cookie domain but no auth url
|
||||||
config = &Config{
|
config.CookieDomains = []CookieDomain{*NewCookieDomain("example.com")}
|
||||||
CookieDomains: []CookieDomain{*NewCookieDomain("example.com")},
|
c = MakeCSRFCookie(r, "12222278901234567890123456789012")
|
||||||
}
|
assert.Equal("_forward_auth_csrf_122222", c.Name)
|
||||||
c = MakeCSRFCookie(r, "12345678901234567890123456789012")
|
|
||||||
assert.Equal("app.example.com", c.Domain)
|
assert.Equal("app.example.com", c.Domain)
|
||||||
|
|
||||||
// With cookie domain and auth url
|
// With cookie domain and auth url
|
||||||
config = &Config{
|
config.AuthHost = "auth.example.com"
|
||||||
AuthHost: "auth.example.com",
|
config.CookieDomains = []CookieDomain{*NewCookieDomain("example.com")}
|
||||||
CookieDomains: []CookieDomain{*NewCookieDomain("example.com")},
|
c = MakeCSRFCookie(r, "12333378901234567890123456789012")
|
||||||
}
|
assert.Equal("_forward_auth_csrf_123333", c.Name)
|
||||||
c = MakeCSRFCookie(r, "12345678901234567890123456789012")
|
|
||||||
assert.Equal("example.com", c.Domain)
|
assert.Equal("example.com", c.Domain)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAuthClearCSRFCookie(t *testing.T) {
|
func TestAuthClearCSRFCookie(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
config, _ = NewConfig([]string{})
|
config, _ = NewConfig([]string{})
|
||||||
r, _ := http.NewRequest("GET", "http://example.com", nil)
|
r, _ := http.NewRequest("GET", "http://example.com", nil)
|
||||||
|
|
||||||
c := ClearCSRFCookie(r)
|
c := ClearCSRFCookie(r, &http.Cookie{Name: "someCsrfCookie"})
|
||||||
|
assert.Equal("someCsrfCookie", c.Name)
|
||||||
if c.Value != "" {
|
if c.Value != "" {
|
||||||
t.Error("ClearCSRFCookie should create cookie with empty value")
|
t.Error("ClearCSRFCookie should create cookie with empty value")
|
||||||
}
|
}
|
||||||
@ -225,63 +321,62 @@ func TestAuthValidateCSRFCookie(t *testing.T) {
|
|||||||
assert := assert.New(t)
|
assert := assert.New(t)
|
||||||
config, _ = NewConfig([]string{})
|
config, _ = NewConfig([]string{})
|
||||||
c := &http.Cookie{}
|
c := &http.Cookie{}
|
||||||
|
state := ""
|
||||||
newCsrfRequest := func(state string) *http.Request {
|
|
||||||
u := fmt.Sprintf("http://example.com?state=%s", state)
|
|
||||||
r, _ := http.NewRequest("GET", u, nil)
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
|
|
||||||
// Should require 32 char string
|
// Should require 32 char string
|
||||||
r := newCsrfRequest("")
|
state = ""
|
||||||
c.Value = ""
|
c.Value = ""
|
||||||
valid, _, _, err := ValidateCSRFCookie(r, c)
|
valid, _, _, err := ValidateCSRFCookie(c, state)
|
||||||
assert.False(valid)
|
assert.False(valid)
|
||||||
if assert.Error(err) {
|
if assert.Error(err) {
|
||||||
assert.Equal("Invalid CSRF cookie value", err.Error())
|
assert.Equal("Invalid CSRF cookie value", err.Error())
|
||||||
}
|
}
|
||||||
c.Value = "123456789012345678901234567890123"
|
c.Value = "123456789012345678901234567890123"
|
||||||
valid, _, _, err = ValidateCSRFCookie(r, c)
|
valid, _, _, err = ValidateCSRFCookie(c, state)
|
||||||
assert.False(valid)
|
assert.False(valid)
|
||||||
if assert.Error(err) {
|
if assert.Error(err) {
|
||||||
assert.Equal("Invalid CSRF cookie value", err.Error())
|
assert.Equal("Invalid CSRF cookie value", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Should require valid state
|
|
||||||
r = newCsrfRequest("12345678901234567890123456789012:")
|
|
||||||
c.Value = "12345678901234567890123456789012"
|
|
||||||
valid, _, _, err = ValidateCSRFCookie(r, c)
|
|
||||||
assert.False(valid)
|
|
||||||
if assert.Error(err) {
|
|
||||||
assert.Equal("Invalid CSRF state value", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Should require provider
|
// Should require provider
|
||||||
r = newCsrfRequest("12345678901234567890123456789012:99")
|
state = "12345678901234567890123456789012:99"
|
||||||
c.Value = "12345678901234567890123456789012"
|
c.Value = "12345678901234567890123456789012"
|
||||||
valid, _, _, err = ValidateCSRFCookie(r, c)
|
valid, _, _, err = ValidateCSRFCookie(c, state)
|
||||||
assert.False(valid)
|
assert.False(valid)
|
||||||
if assert.Error(err) {
|
if assert.Error(err) {
|
||||||
assert.Equal("Invalid CSRF state format", err.Error())
|
assert.Equal("Invalid CSRF state format", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Should allow valid state
|
// Should allow valid state
|
||||||
r = newCsrfRequest("12345678901234567890123456789012:p99:url123")
|
state = "12345678901234567890123456789012:p99:url123"
|
||||||
c.Value = "12345678901234567890123456789012"
|
c.Value = "12345678901234567890123456789012"
|
||||||
valid, provider, redirect, err := ValidateCSRFCookie(r, c)
|
valid, provider, redirect, err := ValidateCSRFCookie(c, state)
|
||||||
assert.True(valid, "valid request should return valid")
|
assert.True(valid, "valid request should return valid")
|
||||||
assert.Nil(err, "valid request should not return an error")
|
assert.Nil(err, "valid request should not return an error")
|
||||||
assert.Equal("p99", provider, "valid request should return correct provider")
|
assert.Equal("p99", provider, "valid request should return correct provider")
|
||||||
assert.Equal("url123", redirect, "valid request should return correct redirect")
|
assert.Equal("url123", redirect, "valid request should return correct redirect")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestValidateState(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
// Should require valid state
|
||||||
|
state := "12345678901234567890123456789012:"
|
||||||
|
err := ValidateState(state)
|
||||||
|
if assert.Error(err) {
|
||||||
|
assert.Equal("Invalid CSRF state value", err.Error())
|
||||||
|
}
|
||||||
|
// Should pass this state
|
||||||
|
state = "12345678901234567890123456789012:p99:url123"
|
||||||
|
err = ValidateState(state)
|
||||||
|
assert.Nil(err, "valid request should not return an error")
|
||||||
|
}
|
||||||
|
|
||||||
func TestMakeState(t *testing.T) {
|
func TestMakeState(t *testing.T) {
|
||||||
assert := assert.New(t)
|
assert := assert.New(t)
|
||||||
|
|
||||||
r, _ := http.NewRequest("GET", "http://example.com", nil)
|
r := httptest.NewRequest("GET", "http://example.com/hello", nil)
|
||||||
r.Header.Add("X-Forwarded-Proto", "http")
|
r.Header.Add("X-Forwarded-Proto", "http")
|
||||||
r.Header.Add("X-Forwarded-Host", "example.com")
|
|
||||||
r.Header.Add("X-Forwarded-Uri", "/hello")
|
|
||||||
|
|
||||||
// Test with google
|
// Test with google
|
||||||
p := provider.Google{}
|
p := provider.Google{}
|
||||||
@ -292,6 +387,11 @@ func TestMakeState(t *testing.T) {
|
|||||||
p2 := provider.OIDC{}
|
p2 := provider.OIDC{}
|
||||||
state = MakeState(r, &p2, "nonce")
|
state = MakeState(r, &p2, "nonce")
|
||||||
assert.Equal("nonce:oidc:http://example.com/hello", state)
|
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) {
|
func TestAuthNonce(t *testing.T) {
|
||||||
|
@ -19,23 +19,27 @@ import (
|
|||||||
|
|
||||||
var config *Config
|
var config *Config
|
||||||
|
|
||||||
|
// Config holds the runtime application config
|
||||||
type Config struct {
|
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"`
|
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"`
|
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"`
|
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:"-"`
|
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"`
|
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"`
|
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"`
|
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"`
|
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"`
|
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"`
|
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" description:"Only allow given email domains, can be set multiple times"`
|
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"`
|
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"`
|
LogoutRedirect string `long:"logout-redirect" env:"LOGOUT_REDIRECT" description:"URL to redirect to following logout"`
|
||||||
SecretString string `long:"secret" env:"SECRET" description:"Secret used for signing (required)" json:"-"`
|
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)"`
|
||||||
Whitelist CommaSeparatedList `long:"whitelist" env:"WHITELIST" description:"Only allow given email addresses, can be set multiple times"`
|
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"`
|
||||||
|
Port int `long:"port" env:"PORT" default:"4181" description:"Port to listen on"`
|
||||||
|
|
||||||
Providers provider.Providers `group:"providers" namespace:"providers" env-namespace:"PROVIDERS"`
|
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\""`
|
Rules map[string]*Rule `long:"rule.<name>.<param>" description:"Rule definitions, param can be: \"action\", \"rule\" or \"provider\""`
|
||||||
@ -44,6 +48,9 @@ type Config struct {
|
|||||||
Secret []byte `json:"-"`
|
Secret []byte `json:"-"`
|
||||||
Lifetime time.Duration
|
Lifetime time.Duration
|
||||||
|
|
||||||
|
// Authorization
|
||||||
|
RequiredRole string `long:"required-role" env:"REQUIRED_ROLE" description:"Required role to verify authorization"`
|
||||||
|
|
||||||
// Legacy
|
// Legacy
|
||||||
CookieDomainsLegacy CookieDomains `long:"cookie-domains" env:"COOKIE_DOMAINS" description:"DEPRECATED - Use \"cookie-domain\""`
|
CookieDomainsLegacy CookieDomains `long:"cookie-domains" env:"COOKIE_DOMAINS" description:"DEPRECATED - Use \"cookie-domain\""`
|
||||||
CookieSecretLegacy string `long:"cookie-secret" env:"COOKIE_SECRET" description:"DEPRECATED - Use \"secret\"" json:"-"`
|
CookieSecretLegacy string `long:"cookie-secret" env:"COOKIE_SECRET" description:"DEPRECATED - Use \"secret\"" json:"-"`
|
||||||
@ -53,6 +60,7 @@ type Config struct {
|
|||||||
PromptLegacy string `long:"prompt" env:"PROMPT" description:"DEPRECATED - Use \"providers.google.prompt\""`
|
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 {
|
func NewGlobalConfig() *Config {
|
||||||
var err error
|
var err error
|
||||||
config, err = NewConfig(os.Args[1:])
|
config, err = NewConfig(os.Args[1:])
|
||||||
@ -66,6 +74,7 @@ func NewGlobalConfig() *Config {
|
|||||||
|
|
||||||
// TODO: move config parsing into new func "NewParsedConfig"
|
// TODO: move config parsing into new func "NewParsedConfig"
|
||||||
|
|
||||||
|
// NewConfig parses and validates provided configuration into a config object
|
||||||
func NewConfig(args []string) (*Config, error) {
|
func NewConfig(args []string) (*Config, error) {
|
||||||
c := &Config{
|
c := &Config{
|
||||||
Rules: map[string]*Rule{},
|
Rules: map[string]*Rule{},
|
||||||
@ -205,6 +214,14 @@ func (c *Config) parseUnknownFlag(option string, arg flags.SplitArgument, args [
|
|||||||
rule.Rule = val
|
rule.Rule = val
|
||||||
case "provider":
|
case "provider":
|
||||||
rule.Provider = val
|
rule.Provider = val
|
||||||
|
case "whitelist":
|
||||||
|
list := CommaSeparatedList{}
|
||||||
|
list.UnmarshalFlag(val)
|
||||||
|
rule.Whitelist = list
|
||||||
|
case "domains":
|
||||||
|
list := CommaSeparatedList{}
|
||||||
|
list.UnmarshalFlag(val)
|
||||||
|
rule.Domains = list
|
||||||
default:
|
default:
|
||||||
return args, fmt.Errorf("invalid route param: %v", option)
|
return args, fmt.Errorf("invalid route param: %v", option)
|
||||||
}
|
}
|
||||||
@ -236,6 +253,7 @@ func convertLegacyToIni(name string) (io.Reader, error) {
|
|||||||
return bytes.NewReader(legacyFileFormat.ReplaceAll(b, []byte("$1=$2"))), nil
|
return bytes.NewReader(legacyFileFormat.ReplaceAll(b, []byte("$1=$2"))), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate validates a config object
|
||||||
func (c *Config) Validate() {
|
func (c *Config) Validate() {
|
||||||
// Check for show stopper errors
|
// Check for show stopper errors
|
||||||
if len(c.Secret) == 0 {
|
if len(c.Secret) == 0 {
|
||||||
@ -269,6 +287,8 @@ func (c *Config) GetProvider(name string) (provider.Provider, error) {
|
|||||||
return &c.Providers.Google, nil
|
return &c.Providers.Google, nil
|
||||||
case "oidc":
|
case "oidc":
|
||||||
return &c.Providers.OIDC, nil
|
return &c.Providers.OIDC, nil
|
||||||
|
case "generic-oauth":
|
||||||
|
return &c.Providers.GenericOAuth, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, fmt.Errorf("Unknown provider: %s", name)
|
return nil, fmt.Errorf("Unknown provider: %s", name)
|
||||||
@ -309,7 +329,7 @@ func (c *Config) setupProvider(name string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Setup
|
// Setup
|
||||||
err = p.Setup()
|
err = p.Setup(log)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -317,12 +337,16 @@ func (c *Config) setupProvider(name string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Rule holds defined rules
|
||||||
type Rule struct {
|
type Rule struct {
|
||||||
Action string
|
Action string
|
||||||
Rule string
|
Rule string
|
||||||
Provider string
|
Provider string
|
||||||
|
Whitelist CommaSeparatedList
|
||||||
|
Domains CommaSeparatedList
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewRule creates a new rule object
|
||||||
func NewRule() *Rule {
|
func NewRule() *Rule {
|
||||||
return &Rule{
|
return &Rule{
|
||||||
Action: "auth",
|
Action: "auth",
|
||||||
@ -335,6 +359,7 @@ func (r *Rule) formattedRule() string {
|
|||||||
return strings.ReplaceAll(r.Rule, "Host(", "HostRegexp(")
|
return strings.ReplaceAll(r.Rule, "Host(", "HostRegexp(")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate validates a rule
|
||||||
func (r *Rule) Validate(c *Config) error {
|
func (r *Rule) Validate(c *Config) error {
|
||||||
if r.Action != "auth" && r.Action != "allow" {
|
if r.Action != "auth" && r.Action != "allow" {
|
||||||
return errors.New("invalid rule action, must be \"auth\" or \"allow\"")
|
return errors.New("invalid rule action, must be \"auth\" or \"allow\"")
|
||||||
@ -345,13 +370,16 @@ func (r *Rule) Validate(c *Config) error {
|
|||||||
|
|
||||||
// Legacy support for comma separated lists
|
// Legacy support for comma separated lists
|
||||||
|
|
||||||
|
// CommaSeparatedList provides legacy support for config values provided as csv
|
||||||
type CommaSeparatedList []string
|
type CommaSeparatedList []string
|
||||||
|
|
||||||
|
// UnmarshalFlag converts a comma separated list to an array
|
||||||
func (c *CommaSeparatedList) UnmarshalFlag(value string) error {
|
func (c *CommaSeparatedList) UnmarshalFlag(value string) error {
|
||||||
*c = append(*c, strings.Split(value, ",")...)
|
*c = append(*c, strings.Split(value, ",")...)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MarshalFlag converts an array back to a comma separated list
|
||||||
func (c *CommaSeparatedList) MarshalFlag() (string, error) {
|
func (c *CommaSeparatedList) MarshalFlag() (string, error) {
|
||||||
return strings.Join(*c, ","), nil
|
return strings.Join(*c, ","), nil
|
||||||
}
|
}
|
||||||
|
@ -33,8 +33,13 @@ func TestConfigDefaults(t *testing.T) {
|
|||||||
assert.Equal("google", c.DefaultProvider)
|
assert.Equal("google", c.DefaultProvider)
|
||||||
assert.Len(c.Domains, 0)
|
assert.Len(c.Domains, 0)
|
||||||
assert.Equal(time.Second*time.Duration(43200), c.Lifetime)
|
assert.Equal(time.Second*time.Duration(43200), c.Lifetime)
|
||||||
|
assert.Equal("", c.LogoutRedirect)
|
||||||
|
assert.False(c.MatchWhitelistOrDomain)
|
||||||
assert.Equal("/_oauth", c.Path)
|
assert.Equal("/_oauth", c.Path)
|
||||||
assert.Len(c.Whitelist, 0)
|
assert.Len(c.Whitelist, 0)
|
||||||
|
assert.Equal(c.Port, 4181)
|
||||||
|
|
||||||
|
assert.Equal("select_account", c.Providers.Google.Prompt)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestConfigParseArgs(t *testing.T) {
|
func TestConfigParseArgs(t *testing.T) {
|
||||||
@ -47,6 +52,7 @@ func TestConfigParseArgs(t *testing.T) {
|
|||||||
"--rule.1.rule=PathPrefix(`/one`)",
|
"--rule.1.rule=PathPrefix(`/one`)",
|
||||||
"--rule.two.action=auth",
|
"--rule.two.action=auth",
|
||||||
"--rule.two.rule=\"Host(`two.com`) && Path(`/two`)\"",
|
"--rule.two.rule=\"Host(`two.com`) && Path(`/two`)\"",
|
||||||
|
"--port=8000",
|
||||||
})
|
})
|
||||||
require.Nil(t, err)
|
require.Nil(t, err)
|
||||||
|
|
||||||
@ -54,6 +60,7 @@ func TestConfigParseArgs(t *testing.T) {
|
|||||||
assert.Equal("cookiename", c.CookieName)
|
assert.Equal("cookiename", c.CookieName)
|
||||||
assert.Equal("csrfcookiename", c.CSRFCookieName)
|
assert.Equal("csrfcookiename", c.CSRFCookieName)
|
||||||
assert.Equal("oidc", c.DefaultProvider)
|
assert.Equal("oidc", c.DefaultProvider)
|
||||||
|
assert.Equal(8000, c.Port)
|
||||||
|
|
||||||
// Check rules
|
// Check rules
|
||||||
assert.Equal(map[string]*Rule{
|
assert.Equal(map[string]*Rule{
|
||||||
@ -197,14 +204,27 @@ func TestConfigParseEnvironment(t *testing.T) {
|
|||||||
assert := assert.New(t)
|
assert := assert.New(t)
|
||||||
os.Setenv("COOKIE_NAME", "env_cookie_name")
|
os.Setenv("COOKIE_NAME", "env_cookie_name")
|
||||||
os.Setenv("PROVIDERS_GOOGLE_CLIENT_ID", "env_client_id")
|
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{})
|
c, err := NewConfig([]string{})
|
||||||
assert.Nil(err)
|
assert.Nil(err)
|
||||||
|
|
||||||
assert.Equal("env_cookie_name", c.CookieName, "variable should be read from environment")
|
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("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("COOKIE_NAME")
|
||||||
os.Unsetenv("PROVIDERS_GOOGLE_CLIENT_ID")
|
os.Unsetenv("PROVIDERS_GOOGLE_CLIENT_ID")
|
||||||
|
os.Unsetenv("COOKIE_DOMAIN")
|
||||||
|
os.Unsetenv("DOMAIN")
|
||||||
|
os.Unsetenv("WHITELIST")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestConfigParseEnvironmentBackwardsCompatability(t *testing.T) {
|
func TestConfigParseEnvironmentBackwardsCompatability(t *testing.T) {
|
||||||
@ -349,6 +369,11 @@ func TestConfigGetProvider(t *testing.T) {
|
|||||||
assert.Nil(err)
|
assert.Nil(err)
|
||||||
assert.Equal(&c.Providers.OIDC, p)
|
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
|
// Should catch unknown provider
|
||||||
p, err = c.GetProvider("bad")
|
p, err = c.GetProvider("bad")
|
||||||
if assert.Error(err) {
|
if assert.Error(err) {
|
||||||
|
@ -8,6 +8,7 @@ import (
|
|||||||
|
|
||||||
var log *logrus.Logger
|
var log *logrus.Logger
|
||||||
|
|
||||||
|
// NewDefaultLogger creates a new logger based on the current configuration
|
||||||
func NewDefaultLogger() *logrus.Logger {
|
func NewDefaultLogger() *logrus.Logger {
|
||||||
// Setup logger
|
// Setup logger
|
||||||
log = logrus.StandardLogger()
|
log = logrus.StandardLogger()
|
||||||
|
99
internal/provider/generic_oauth.go
Normal file
99
internal/provider/generic_oauth.go
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
package provider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"golang.org/x/oauth2"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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(log *logrus.Logger) 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, Roles, error) {
|
||||||
|
var user User
|
||||||
|
var roles Roles
|
||||||
|
|
||||||
|
req, err := http.NewRequest("GET", o.UserURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return user, roles, 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, roles, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer res.Body.Close()
|
||||||
|
err = json.NewDecoder(res.Body).Decode(&user)
|
||||||
|
|
||||||
|
return user, roles, err
|
||||||
|
}
|
140
internal/provider/generic_oauth_test.go
Normal file
140
internal/provider/generic_oauth_test.go
Normal 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)
|
||||||
|
}
|
@ -6,6 +6,8 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Google provider
|
// Google provider
|
||||||
@ -13,7 +15,7 @@ type Google struct {
|
|||||||
ClientID string `long:"client-id" env:"CLIENT_ID" description:"Client ID"`
|
ClientID string `long:"client-id" env:"CLIENT_ID" description:"Client ID"`
|
||||||
ClientSecret string `long:"client-secret" env:"CLIENT_SECRET" description:"Client Secret" json:"-"`
|
ClientSecret string `long:"client-secret" env:"CLIENT_SECRET" description:"Client Secret" json:"-"`
|
||||||
Scope string
|
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
|
LoginURL *url.URL
|
||||||
TokenURL *url.URL
|
TokenURL *url.URL
|
||||||
@ -26,7 +28,7 @@ func (g *Google) Name() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Setup performs validation and setup
|
// Setup performs validation and setup
|
||||||
func (g *Google) Setup() error {
|
func (g *Google) Setup(log *logrus.Logger) error {
|
||||||
if g.ClientID == "" || g.ClientSecret == "" {
|
if g.ClientID == "" || g.ClientSecret == "" {
|
||||||
return errors.New("providers.google.client-id, providers.google.client-secret must be set")
|
return errors.New("providers.google.client-id, providers.google.client-secret must be set")
|
||||||
}
|
}
|
||||||
@ -93,23 +95,24 @@ func (g *Google) ExchangeCode(redirectURI, code string) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetUser uses the given token and returns a complete provider.User object
|
// GetUser uses the given token and returns a complete provider.User object
|
||||||
func (g *Google) GetUser(token string) (User, error) {
|
func (g *Google) GetUser(token string) (User, Roles, error) {
|
||||||
var user User
|
var user User
|
||||||
|
var roles Roles
|
||||||
|
|
||||||
client := &http.Client{}
|
client := &http.Client{}
|
||||||
req, err := http.NewRequest("GET", g.UserURL.String(), nil)
|
req, err := http.NewRequest("GET", g.UserURL.String(), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return user, err
|
return user, roles, err
|
||||||
}
|
}
|
||||||
|
|
||||||
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
|
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
|
||||||
res, err := client.Do(req)
|
res, err := client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return user, err
|
return user, roles, err
|
||||||
}
|
}
|
||||||
|
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
err = json.NewDecoder(res.Body).Decode(&user)
|
err = json.NewDecoder(res.Body).Decode(&user)
|
||||||
|
|
||||||
return user, err
|
return user, roles, err
|
||||||
}
|
}
|
||||||
|
@ -144,8 +144,5 @@ func TestGoogleGetUser(t *testing.T) {
|
|||||||
user, err := p.GetUser("123456789")
|
user, err := p.GetUser("123456789")
|
||||||
assert.Nil(err)
|
assert.Nil(err)
|
||||||
|
|
||||||
assert.Equal("1", user.ID)
|
|
||||||
assert.Equal("example@example.com", user.Email)
|
assert.Equal("example@example.com", user.Email)
|
||||||
assert.True(user.Verified)
|
|
||||||
assert.Equal("example.com", user.Hd)
|
|
||||||
}
|
}
|
||||||
|
@ -6,18 +6,22 @@ import (
|
|||||||
|
|
||||||
"github.com/coreos/go-oidc"
|
"github.com/coreos/go-oidc"
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
// OIDC provider
|
// OIDC provider
|
||||||
type OIDC struct {
|
type OIDC struct {
|
||||||
OAuthProvider
|
|
||||||
|
|
||||||
IssuerURL string `long:"issuer-url" env:"ISSUER_URL" description:"Issuer URL"`
|
IssuerURL string `long:"issuer-url" env:"ISSUER_URL" description:"Issuer URL"`
|
||||||
ClientID string `long:"client-id" env:"CLIENT_ID" description:"Client ID"`
|
ClientID string `long:"client-id" env:"CLIENT_ID" description:"Client ID"`
|
||||||
ClientSecret string `long:"client-secret" env:"CLIENT_SECRET" description:"Client Secret" json:"-"`
|
ClientSecret string `long:"client-secret" env:"CLIENT_SECRET" description:"Client Secret" json:"-"`
|
||||||
|
|
||||||
|
OAuthProvider
|
||||||
|
|
||||||
provider *oidc.Provider
|
provider *oidc.Provider
|
||||||
verifier *oidc.IDTokenVerifier
|
verifier *oidc.IDTokenVerifier
|
||||||
|
|
||||||
|
log *logrus.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
// Name returns the name of the provider
|
// Name returns the name of the provider
|
||||||
@ -26,7 +30,9 @@ func (o *OIDC) Name() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Setup performs validation and setup
|
// Setup performs validation and setup
|
||||||
func (o *OIDC) Setup() error {
|
func (o *OIDC) Setup(log *logrus.Logger) error {
|
||||||
|
o.log = log
|
||||||
|
|
||||||
// Check parms
|
// Check parms
|
||||||
if o.IssuerURL == "" || o.ClientID == "" || o.ClientSecret == "" {
|
if o.IssuerURL == "" || o.ClientID == "" || o.ClientSecret == "" {
|
||||||
return errors.New("providers.oidc.issuer-url, providers.oidc.client-id, providers.oidc.client-secret must be set")
|
return errors.New("providers.oidc.issuer-url, providers.oidc.client-id, providers.oidc.client-secret must be set")
|
||||||
@ -70,6 +76,7 @@ func (o *OIDC) ExchangeCode(redirectURI, code string) (string, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
o.log.WithField("accessToken", token.AccessToken).Debug("getUser")
|
||||||
|
|
||||||
// Extract ID token
|
// Extract ID token
|
||||||
rawIDToken, ok := token.Extra("id_token").(string)
|
rawIDToken, ok := token.Extra("id_token").(string)
|
||||||
@ -81,28 +88,26 @@ func (o *OIDC) ExchangeCode(redirectURI, code string) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetUser uses the given token and returns a complete provider.User object
|
// GetUser uses the given token and returns a complete provider.User object
|
||||||
func (o *OIDC) GetUser(token string) (User, error) {
|
func (o *OIDC) GetUser(token string) (User, Roles, error) {
|
||||||
var user User
|
var user User
|
||||||
|
var roles Roles
|
||||||
|
|
||||||
// Parse & Verify ID Token
|
// Parse & Verify ID Token
|
||||||
idToken, err := o.verifier.Verify(o.ctx, token)
|
idToken, err := o.verifier.Verify(o.ctx, token)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return user, err
|
return user, roles, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract custom claims
|
// Extract custom claims
|
||||||
var claims struct {
|
if err := idToken.Claims(&user); err != nil {
|
||||||
ID string `json:"sub"`
|
return user, roles, err
|
||||||
Email string `json:"email"`
|
|
||||||
Verified bool `json:"email_verified"`
|
|
||||||
}
|
}
|
||||||
if err := idToken.Claims(&claims); err != nil {
|
o.log.WithField("user", user).Debug("getUser")
|
||||||
return user, err
|
|
||||||
|
if err := idToken.Claims(&roles); err != nil {
|
||||||
|
return user, roles, err
|
||||||
}
|
}
|
||||||
|
o.log.WithField("roles", roles).Debug("getUser")
|
||||||
|
|
||||||
user.ID = claims.ID
|
return user, roles, nil
|
||||||
user.Email = claims.Email
|
|
||||||
user.Verified = claims.Verified
|
|
||||||
|
|
||||||
return user, nil
|
|
||||||
}
|
}
|
||||||
|
@ -59,6 +59,33 @@ func TestOIDCGetLoginURL(t *testing.T) {
|
|||||||
|
|
||||||
// Calling the method should not modify the underlying config
|
// Calling the method should not modify the underlying config
|
||||||
assert.Equal("", provider.Config.RedirectURL)
|
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) {
|
func TestOIDCExchangeCode(t *testing.T) {
|
||||||
@ -97,9 +124,7 @@ func TestOIDCGetUser(t *testing.T) {
|
|||||||
// Get user
|
// Get user
|
||||||
user, err := provider.GetUser(token)
|
user, err := provider.GetUser(token)
|
||||||
assert.Nil(err)
|
assert.Nil(err)
|
||||||
assert.Equal("1", user.ID)
|
|
||||||
assert.Equal("example@example.com", user.Email)
|
assert.Equal("example@example.com", user.Email)
|
||||||
assert.True(user.Verified)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
|
@ -5,12 +5,14 @@ import (
|
|||||||
// "net/url"
|
// "net/url"
|
||||||
|
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Providers contains all the implemented providers
|
// Providers contains all the implemented providers
|
||||||
type Providers struct {
|
type Providers struct {
|
||||||
Google Google `group:"Google Provider" namespace:"google" env-namespace:"GOOGLE"`
|
Google Google `group:"Google Provider" namespace:"google" env-namespace:"GOOGLE"`
|
||||||
OIDC OIDC `group:"OIDC Provider" namespace:"oidc" env-namespace:"OIDC"`
|
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
|
// Provider is used to authenticate users
|
||||||
@ -18,8 +20,8 @@ type Provider interface {
|
|||||||
Name() string
|
Name() string
|
||||||
GetLoginURL(redirectURI, state string) string
|
GetLoginURL(redirectURI, state string) string
|
||||||
ExchangeCode(redirectURI, code string) (string, error)
|
ExchangeCode(redirectURI, code string) (string, error)
|
||||||
GetUser(token string) (User, error)
|
GetUser(token string) (User, Roles, error)
|
||||||
Setup() error
|
Setup(*logrus.Logger) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type token struct {
|
type token struct {
|
||||||
@ -28,14 +30,17 @@ type token struct {
|
|||||||
|
|
||||||
// User is the authenticated user
|
// User is the authenticated user
|
||||||
type User struct {
|
type User struct {
|
||||||
ID string `json:"id"`
|
Email string `json:"email"`
|
||||||
Email string `json:"email"`
|
}
|
||||||
Verified bool `json:"verified_email"`
|
|
||||||
Hd string `json:"hd"`
|
type Roles struct {
|
||||||
|
Roles []string `json:"roles"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// OAuthProvider is a provider using the oauth2 library
|
// OAuthProvider is a provider using the oauth2 library
|
||||||
type OAuthProvider struct {
|
type OAuthProvider struct {
|
||||||
|
Resource string `long:"resource" env:"RESOURCE" description:"Optional resource indicator"`
|
||||||
|
|
||||||
Config *oauth2.Config
|
Config *oauth2.Config
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
}
|
}
|
||||||
@ -51,6 +56,11 @@ func (p *OAuthProvider) ConfigCopy(redirectURI string) oauth2.Config {
|
|||||||
// OAuthGetLoginURL provides a base "GetLoginURL" for proiders using OAauth2
|
// OAuthGetLoginURL provides a base "GetLoginURL" for proiders using OAauth2
|
||||||
func (p *OAuthProvider) OAuthGetLoginURL(redirectURI, state string) string {
|
func (p *OAuthProvider) OAuthGetLoginURL(redirectURI, state string) string {
|
||||||
config := p.ConfigCopy(redirectURI)
|
config := p.ConfigCopy(redirectURI)
|
||||||
|
|
||||||
|
if p.Resource != "" {
|
||||||
|
return config.AuthCodeURL(state, oauth2.SetAuthURLParam("resource", p.Resource))
|
||||||
|
}
|
||||||
|
|
||||||
return config.AuthCodeURL(state)
|
return config.AuthCodeURL(state)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,10 +9,12 @@ import (
|
|||||||
"github.com/thomseddon/traefik-forward-auth/internal/provider"
|
"github.com/thomseddon/traefik-forward-auth/internal/provider"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Server contains router and handler methods
|
||||||
type Server struct {
|
type Server struct {
|
||||||
router *rules.Router
|
router *rules.Router
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewServer creates a new server object and builds router
|
||||||
func NewServer() *Server {
|
func NewServer() *Server {
|
||||||
s := &Server{}
|
s := &Server{}
|
||||||
s.buildRoutes()
|
s.buildRoutes()
|
||||||
@ -39,6 +41,9 @@ func (s *Server) buildRoutes() {
|
|||||||
// Add callback handler
|
// Add callback handler
|
||||||
s.router.Handle(config.Path, s.AuthCallbackHandler())
|
s.router.Handle(config.Path, s.AuthCallbackHandler())
|
||||||
|
|
||||||
|
// Add logout handler
|
||||||
|
s.router.Handle(config.Path+"/logout", s.LogoutHandler())
|
||||||
|
|
||||||
// Add a default handler
|
// Add a default handler
|
||||||
if config.DefaultAction == "allow" {
|
if config.DefaultAction == "allow" {
|
||||||
s.router.NewRoute().Handler(s.AllowHandler("default"))
|
s.router.NewRoute().Handler(s.AllowHandler("default"))
|
||||||
@ -47,31 +52,37 @@ 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) {
|
func (s *Server) RootHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
// Modify request
|
// Modify request
|
||||||
r.Method = r.Header.Get("X-Forwarded-Method")
|
r.Method = r.Header.Get("X-Forwarded-Method")
|
||||||
r.Host = r.Header.Get("X-Forwarded-Host")
|
r.Host = r.Header.Get("X-Forwarded-Host")
|
||||||
r.URL, _ = url.Parse(r.Header.Get("X-Forwarded-Uri"))
|
|
||||||
|
// Read URI from header if we're acting as forward auth middleware
|
||||||
|
if _, ok := r.Header["X-Forwarded-Uri"]; ok {
|
||||||
|
r.URL, _ = url.Parse(r.Header.Get("X-Forwarded-Uri"))
|
||||||
|
}
|
||||||
|
|
||||||
// Pass to mux
|
// Pass to mux
|
||||||
s.router.ServeHTTP(w, r)
|
s.router.ServeHTTP(w, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handler that allows requests
|
// AllowHandler Allows requests
|
||||||
func (s *Server) AllowHandler(rule string) http.HandlerFunc {
|
func (s *Server) AllowHandler(rule string) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
s.logger(r, rule, "Allowing request")
|
s.logger(r, "Allow", rule, "Allowing request")
|
||||||
w.WriteHeader(200)
|
w.WriteHeader(200)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Authenticate requests
|
// AuthHandler Authenticates requests
|
||||||
func (s *Server) AuthHandler(providerName, rule string) http.HandlerFunc {
|
func (s *Server) AuthHandler(providerName, rule string) http.HandlerFunc {
|
||||||
p, _ := config.GetConfiguredProvider(providerName)
|
p, _ := config.GetConfiguredProvider(providerName)
|
||||||
|
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
// Logging setup
|
// Logging setup
|
||||||
logger := s.logger(r, rule, "Authenticating request")
|
logger := s.logger(r, "Auth", rule, "Authenticating request")
|
||||||
|
|
||||||
// Get auth cookie
|
// Get auth cookie
|
||||||
c, err := r.Cookie(config.CookieName)
|
c, err := r.Cookie(config.CookieName)
|
||||||
@ -87,47 +98,58 @@ func (s *Server) AuthHandler(providerName, rule string) http.HandlerFunc {
|
|||||||
logger.Info("Cookie has expired")
|
logger.Info("Cookie has expired")
|
||||||
s.authRedirect(logger, w, r, p)
|
s.authRedirect(logger, w, r, p)
|
||||||
} else {
|
} else {
|
||||||
logger.Errorf("Invalid cookie: %v", err)
|
logger.WithField("error", err).Warn("Invalid cookie")
|
||||||
http.Error(w, "Not authorized", 401)
|
http.Error(w, "Not authorized", 401)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate user
|
// Validate user
|
||||||
valid := ValidateEmail(email)
|
valid := ValidateEmail(email, rule)
|
||||||
if !valid {
|
if !valid {
|
||||||
logger.WithFields(logrus.Fields{
|
logger.WithField("email", email).Warn("Invalid email")
|
||||||
"email": email,
|
|
||||||
}).Errorf("Invalid email")
|
|
||||||
http.Error(w, "Not authorized", 401)
|
http.Error(w, "Not authorized", 401)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Valid request
|
// Valid request
|
||||||
logger.Debugf("Allowing valid request ")
|
logger.Debug("Allowing valid request")
|
||||||
w.Header().Set("X-Forwarded-User", email)
|
w.Header().Set("X-Forwarded-User", email)
|
||||||
w.WriteHeader(200)
|
w.WriteHeader(200)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle auth callback
|
// AuthCallbackHandler Handles auth callback request
|
||||||
func (s *Server) AuthCallbackHandler() http.HandlerFunc {
|
func (s *Server) AuthCallbackHandler() http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
// Logging setup
|
// Logging setup
|
||||||
logger := s.logger(r, "default", "Handling callback")
|
logger := s.logger(r, "AuthCallback", "default", "Handling callback")
|
||||||
|
|
||||||
// Check for CSRF cookie
|
// Check state
|
||||||
c, err := r.Cookie(config.CSRFCookieName)
|
state := r.URL.Query().Get("state")
|
||||||
if err != nil {
|
if err := ValidateState(state); err != nil {
|
||||||
logger.Warn("Missing csrf cookie")
|
logger.WithFields(logrus.Fields{
|
||||||
|
"error": err,
|
||||||
|
}).Warn("Error validating state")
|
||||||
http.Error(w, "Not authorized", 401)
|
http.Error(w, "Not authorized", 401)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate state
|
// Check for CSRF cookie
|
||||||
valid, providerName, redirect, err := ValidateCSRFCookie(r, c)
|
c, err := FindCSRFCookie(r, state)
|
||||||
|
if err != nil {
|
||||||
|
logger.Info("Missing csrf cookie")
|
||||||
|
http.Error(w, "Not authorized", 401)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate CSRF cookie against state
|
||||||
|
valid, providerName, redirect, err := ValidateCSRFCookie(c, state)
|
||||||
if !valid {
|
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)
|
http.Error(w, "Not authorized", 401)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -135,71 +157,120 @@ func (s *Server) AuthCallbackHandler() http.HandlerFunc {
|
|||||||
// Get provider
|
// Get provider
|
||||||
p, err := config.GetConfiguredProvider(providerName)
|
p, err := config.GetConfiguredProvider(providerName)
|
||||||
if err != nil {
|
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)
|
http.Error(w, "Not authorized", 401)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear CSRF cookie
|
// Clear CSRF cookie
|
||||||
http.SetCookie(w, ClearCSRFCookie(r))
|
http.SetCookie(w, ClearCSRFCookie(r, c))
|
||||||
|
|
||||||
// Exchange code for token
|
// Exchange code for token
|
||||||
token, err := p.ExchangeCode(redirectUri(r), r.URL.Query().Get("code"))
|
token, err := p.ExchangeCode(redirectUri(r), r.URL.Query().Get("code"))
|
||||||
if err != nil {
|
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)
|
http.Error(w, "Service unavailable", 503)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get user
|
// Get user
|
||||||
user, err := p.GetUser(token)
|
user, roles, err := p.GetUser(token)
|
||||||
if err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
found := false
|
||||||
|
for _, r := range roles.Roles {
|
||||||
|
if r == config.RequiredRole {
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ! found {
|
||||||
|
logger.Debug("required role not found, deny access")
|
||||||
|
http.Error(w, "Forbidden", 403)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Generate cookie
|
// Generate cookie
|
||||||
http.SetCookie(w, MakeCookie(r, user.Email))
|
http.SetCookie(w, MakeCookie(r, user.Email))
|
||||||
logger.WithFields(logrus.Fields{
|
logger.WithFields(logrus.Fields{
|
||||||
"user": user.Email,
|
"provider": providerName,
|
||||||
}).Infof("Generated auth cookie")
|
"redirect": redirect,
|
||||||
|
"user": user.Email,
|
||||||
|
"roles": roles.Roles,
|
||||||
|
}).Info("Successfully generated auth cookie, redirecting user.")
|
||||||
|
|
||||||
// Redirect
|
// Redirect
|
||||||
http.Redirect(w, r, redirect, http.StatusTemporaryRedirect)
|
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) {
|
func (s *Server) authRedirect(logger *logrus.Entry, w http.ResponseWriter, r *http.Request, p provider.Provider) {
|
||||||
// Error indicates no cookie, generate nonce
|
// Error indicates no cookie, generate nonce
|
||||||
err, nonce := Nonce()
|
err, nonce := Nonce()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Errorf("Error generating nonce, %v", err)
|
logger.WithField("error", err).Error("Error generating nonce")
|
||||||
http.Error(w, "Service unavailable", 503)
|
http.Error(w, "Service unavailable", 503)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the CSRF cookie
|
// Set the CSRF cookie
|
||||||
http.SetCookie(w, MakeCSRFCookie(r, nonce))
|
csrf := MakeCSRFCookie(r, nonce)
|
||||||
logger.Debug("Set CSRF cookie and redirecting to google login")
|
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
|
// Forward them on
|
||||||
loginUrl := p.GetLoginURL(redirectUri(r), MakeState(r, p, nonce))
|
loginURL := p.GetLoginURL(redirectUri(r), MakeState(r, p, nonce))
|
||||||
http.Redirect(w, r, loginUrl, http.StatusTemporaryRedirect)
|
http.Redirect(w, r, loginURL, http.StatusTemporaryRedirect)
|
||||||
|
|
||||||
logger.Debug("Done")
|
logger.WithFields(logrus.Fields{
|
||||||
return
|
"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
|
// Create logger
|
||||||
logger := log.WithFields(logrus.Fields{
|
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"),
|
"source_ip": r.Header.Get("X-Forwarded-For"),
|
||||||
})
|
})
|
||||||
|
|
||||||
// Log request
|
// Log request
|
||||||
logger.WithFields(logrus.Fields{
|
logger.WithFields(logrus.Fields{
|
||||||
"rule": rule,
|
"cookies": r.Cookies(),
|
||||||
"headers": r.Header,
|
|
||||||
}).Debug(msg)
|
}).Debug(msg)
|
||||||
|
|
||||||
return logger
|
return logger
|
||||||
|
@ -10,6 +10,8 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"github.com/sirupsen/logrus/hooks/test"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
@ -29,9 +31,42 @@ func init() {
|
|||||||
* Tests
|
* Tests
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
func TestServerRootHandler(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
config = newDefaultConfig()
|
||||||
|
|
||||||
|
// X-Forwarded headers should be read into request
|
||||||
|
req := httptest.NewRequest("POST", "http://should-use-x-forwarded.com/should?ignore=me", nil)
|
||||||
|
req.Header.Add("X-Forwarded-Method", "GET")
|
||||||
|
req.Header.Add("X-Forwarded-Proto", "https")
|
||||||
|
req.Header.Add("X-Forwarded-Host", "example.com")
|
||||||
|
req.Header.Add("X-Forwarded-Uri", "/foo?q=bar")
|
||||||
|
NewServer().RootHandler(httptest.NewRecorder(), req)
|
||||||
|
|
||||||
|
assert.Equal("GET", req.Method, "x-forwarded-method should be read into request")
|
||||||
|
assert.Equal("example.com", req.Host, "x-forwarded-host should be read into request")
|
||||||
|
assert.Equal("/foo", req.URL.Path, "x-forwarded-uri should be read into request")
|
||||||
|
assert.Equal("/foo?q=bar", req.URL.RequestURI(), "x-forwarded-uri should be read into request")
|
||||||
|
|
||||||
|
// Other X-Forwarded headers should be read in into request and original URL
|
||||||
|
// should be preserved if X-Forwarded-Uri not present
|
||||||
|
req = httptest.NewRequest("POST", "http://should-use-x-forwarded.com/should-not?ignore=me", nil)
|
||||||
|
req.Header.Add("X-Forwarded-Method", "GET")
|
||||||
|
req.Header.Add("X-Forwarded-Proto", "https")
|
||||||
|
req.Header.Add("X-Forwarded-Host", "example.com")
|
||||||
|
NewServer().RootHandler(httptest.NewRecorder(), req)
|
||||||
|
|
||||||
|
assert.Equal("GET", req.Method, "x-forwarded-method should be read into request")
|
||||||
|
assert.Equal("example.com", req.Host, "x-forwarded-host should be read into request")
|
||||||
|
assert.Equal("/should-not", req.URL.Path, "request url should be preserved if x-forwarded-uri not present")
|
||||||
|
assert.Equal("/should-not?ignore=me", req.URL.RequestURI(), "request url should be preserved if x-forwarded-uri not present")
|
||||||
|
}
|
||||||
|
|
||||||
func TestServerAuthHandlerInvalid(t *testing.T) {
|
func TestServerAuthHandlerInvalid(t *testing.T) {
|
||||||
assert := assert.New(t)
|
assert := assert.New(t)
|
||||||
config = newDefaultConfig()
|
config = newDefaultConfig()
|
||||||
|
var hook *test.Hook
|
||||||
|
log, hook = test.NewNullLogger()
|
||||||
|
|
||||||
// Should redirect vanilla request to login url
|
// Should redirect vanilla request to login url
|
||||||
req := newDefaultHttpRequest("/foo")
|
req := newDefaultHttpRequest("/foo")
|
||||||
@ -53,6 +88,14 @@ func TestServerAuthHandlerInvalid(t *testing.T) {
|
|||||||
assert.Equal("google", parts[1])
|
assert.Equal("google", parts[1])
|
||||||
assert.Equal("http://example.com/foo", parts[2])
|
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
|
// Should catch invalid cookie
|
||||||
req = newDefaultHttpRequest("/foo")
|
req = newDefaultHttpRequest("/foo")
|
||||||
c := MakeCookie(req, "test@example.com")
|
c := MakeCookie(req, "test@example.com")
|
||||||
@ -78,15 +121,15 @@ func TestServerAuthHandlerExpired(t *testing.T) {
|
|||||||
config.Domains = []string{"test.com"}
|
config.Domains = []string{"test.com"}
|
||||||
|
|
||||||
// Should redirect expired cookie
|
// Should redirect expired cookie
|
||||||
req := newDefaultHttpRequest("/foo")
|
req := newHTTPRequest("GET", "http://example.com/foo")
|
||||||
c := MakeCookie(req, "test@example.com")
|
c := MakeCookie(req, "test@example.com")
|
||||||
res, _ := doHttpRequest(req, c)
|
res, _ := doHttpRequest(req, c)
|
||||||
assert.Equal(307, res.StatusCode, "request with expired cookie should be redirected")
|
require.Equal(t, 307, res.StatusCode, "request with expired cookie should be redirected")
|
||||||
|
|
||||||
// Check for CSRF cookie
|
// Check for CSRF cookie
|
||||||
var cookie *http.Cookie
|
var cookie *http.Cookie
|
||||||
for _, c := range res.Cookies() {
|
for _, c := range res.Cookies() {
|
||||||
if c.Name == config.CSRFCookieName {
|
if strings.HasPrefix(c.Name, config.CSRFCookieName) {
|
||||||
cookie = c
|
cookie = c
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -104,7 +147,7 @@ func TestServerAuthHandlerValid(t *testing.T) {
|
|||||||
config = newDefaultConfig()
|
config = newDefaultConfig()
|
||||||
|
|
||||||
// Should allow valid request email
|
// Should allow valid request email
|
||||||
req := newDefaultHttpRequest("/foo")
|
req := newHTTPRequest("GET", "http://example.com/foo")
|
||||||
c := MakeCookie(req, "test@example.com")
|
c := MakeCookie(req, "test@example.com")
|
||||||
config.Domains = []string{}
|
config.Domains = []string{}
|
||||||
|
|
||||||
@ -119,6 +162,7 @@ func TestServerAuthHandlerValid(t *testing.T) {
|
|||||||
|
|
||||||
func TestServerAuthCallback(t *testing.T) {
|
func TestServerAuthCallback(t *testing.T) {
|
||||||
assert := assert.New(t)
|
assert := assert.New(t)
|
||||||
|
require := require.New(t)
|
||||||
config = newDefaultConfig()
|
config = newDefaultConfig()
|
||||||
|
|
||||||
// Setup OAuth server
|
// Setup OAuth server
|
||||||
@ -136,21 +180,28 @@ func TestServerAuthCallback(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Should pass auth response request to callback
|
// Should pass auth response request to callback
|
||||||
req := newDefaultHttpRequest("/_oauth")
|
req := newHTTPRequest("GET", "http://example.com/_oauth")
|
||||||
res, _ := doHttpRequest(req, nil)
|
res, _ := doHttpRequest(req, nil)
|
||||||
assert.Equal(401, res.StatusCode, "auth callback without cookie shouldn't be authorised")
|
assert.Equal(401, res.StatusCode, "auth callback without cookie shouldn't be authorised")
|
||||||
|
|
||||||
// Should catch invalid csrf cookie
|
// Should catch invalid csrf cookie
|
||||||
req = newDefaultHttpRequest("/_oauth?state=12345678901234567890123456789012:http://redirect")
|
nonce := "12345678901234567890123456789012"
|
||||||
|
req = newHTTPRequest("GET", "http://example.com/_oauth?state="+nonce+":http://redirect")
|
||||||
c := MakeCSRFCookie(req, "nononononononononononononononono")
|
c := MakeCSRFCookie(req, "nononononononononononononononono")
|
||||||
res, _ = doHttpRequest(req, c)
|
res, _ = doHttpRequest(req, c)
|
||||||
assert.Equal(401, res.StatusCode, "auth callback with invalid cookie shouldn't be authorised")
|
assert.Equal(401, res.StatusCode, "auth callback with invalid cookie shouldn't be authorised")
|
||||||
|
|
||||||
// Should redirect valid request
|
// Should catch invalid provider cookie
|
||||||
req = newDefaultHttpRequest("/_oauth?state=12345678901234567890123456789012:google:http://redirect")
|
req = newHTTPRequest("GET", "http://example.com/_oauth?state="+nonce+":invalid:http://redirect")
|
||||||
c = MakeCSRFCookie(req, "12345678901234567890123456789012")
|
c = MakeCSRFCookie(req, nonce)
|
||||||
res, _ = doHttpRequest(req, c)
|
res, _ = doHttpRequest(req, c)
|
||||||
assert.Equal(307, res.StatusCode, "valid auth callback should be allowed")
|
assert.Equal(401, res.StatusCode, "auth callback with invalid provider shouldn't be authorised")
|
||||||
|
|
||||||
|
// Should redirect valid request
|
||||||
|
req = newHTTPRequest("GET", "http://example.com/_oauth?state="+nonce+":google:http://redirect")
|
||||||
|
c = MakeCSRFCookie(req, nonce)
|
||||||
|
res, _ = doHttpRequest(req, c)
|
||||||
|
require.Equal(307, res.StatusCode, "valid auth callback should be allowed")
|
||||||
|
|
||||||
fwd, _ := res.Location()
|
fwd, _ := res.Location()
|
||||||
assert.Equal("http", fwd.Scheme, "valid request should be redirected to return url")
|
assert.Equal("http", fwd.Scheme, "valid request should be redirected to return url")
|
||||||
@ -158,6 +209,101 @@ func TestServerAuthCallback(t *testing.T) {
|
|||||||
assert.Equal("", fwd.Path, "valid request should be redirected to return url")
|
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) {
|
func TestServerDefaultAction(t *testing.T) {
|
||||||
assert := assert.New(t)
|
assert := assert.New(t)
|
||||||
config = newDefaultConfig()
|
config = newDefaultConfig()
|
||||||
@ -247,17 +393,17 @@ func TestServerRouteHost(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Should block any request
|
// Should block any request
|
||||||
req := newHttpRequest("GET", "https://example.com/", "/")
|
req := newHTTPRequest("GET", "https://example.com/")
|
||||||
res, _ := doHttpRequest(req, nil)
|
res, _ := doHttpRequest(req, nil)
|
||||||
assert.Equal(307, res.StatusCode, "request not matching any rule should require auth")
|
assert.Equal(307, res.StatusCode, "request not matching any rule should require auth")
|
||||||
|
|
||||||
// Should allow matching request
|
// Should allow matching request
|
||||||
req = newHttpRequest("GET", "https://api.example.com/", "/")
|
req = newHTTPRequest("GET", "https://api.example.com/")
|
||||||
res, _ = doHttpRequest(req, nil)
|
res, _ = doHttpRequest(req, nil)
|
||||||
assert.Equal(200, res.StatusCode, "request matching allow rule should be allowed")
|
assert.Equal(200, res.StatusCode, "request matching allow rule should be allowed")
|
||||||
|
|
||||||
// Should allow matching request
|
// Should allow matching request
|
||||||
req = newHttpRequest("GET", "https://sub8.example.com/", "/")
|
req = newHTTPRequest("GET", "https://sub8.example.com/")
|
||||||
res, _ = doHttpRequest(req, nil)
|
res, _ = doHttpRequest(req, nil)
|
||||||
assert.Equal(200, res.StatusCode, "request matching allow rule should be allowed")
|
assert.Equal(200, res.StatusCode, "request matching allow rule should be allowed")
|
||||||
}
|
}
|
||||||
@ -273,12 +419,12 @@ func TestServerRouteMethod(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Should block any request
|
// Should block any request
|
||||||
req := newHttpRequest("GET", "https://example.com/", "/")
|
req := newHTTPRequest("GET", "https://example.com/")
|
||||||
res, _ := doHttpRequest(req, nil)
|
res, _ := doHttpRequest(req, nil)
|
||||||
assert.Equal(307, res.StatusCode, "request not matching any rule should require auth")
|
assert.Equal(307, res.StatusCode, "request not matching any rule should require auth")
|
||||||
|
|
||||||
// Should allow matching request
|
// Should allow matching request
|
||||||
req = newHttpRequest("PUT", "https://example.com/", "/")
|
req = newHTTPRequest("PUT", "https://example.com/")
|
||||||
res, _ = doHttpRequest(req, nil)
|
res, _ = doHttpRequest(req, nil)
|
||||||
assert.Equal(200, res.StatusCode, "request matching allow rule should be allowed")
|
assert.Equal(200, res.StatusCode, "request matching allow rule should be allowed")
|
||||||
}
|
}
|
||||||
@ -328,12 +474,12 @@ func TestServerRouteQuery(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Should block any request
|
// Should block any request
|
||||||
req := newHttpRequest("GET", "https://example.com/", "/?q=no")
|
req := newHTTPRequest("GET", "https://example.com/?q=no")
|
||||||
res, _ := doHttpRequest(req, nil)
|
res, _ := doHttpRequest(req, nil)
|
||||||
assert.Equal(307, res.StatusCode, "request not matching any rule should require auth")
|
assert.Equal(307, res.StatusCode, "request not matching any rule should require auth")
|
||||||
|
|
||||||
// Should allow matching request
|
// Should allow matching request
|
||||||
req = newHttpRequest("GET", "https://api.example.com/", "/?q=test123")
|
req = newHTTPRequest("GET", "https://api.example.com/?q=test123")
|
||||||
res, _ = doHttpRequest(req, nil)
|
res, _ = doHttpRequest(req, nil)
|
||||||
assert.Equal(200, res.StatusCode, "request matching allow rule should be allowed")
|
assert.Equal(200, res.StatusCode, "request matching allow rule should be allowed")
|
||||||
}
|
}
|
||||||
@ -343,10 +489,16 @@ func TestServerRouteQuery(t *testing.T) {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
type OAuthServer struct {
|
type OAuthServer struct {
|
||||||
t *testing.T
|
t *testing.T
|
||||||
|
fail bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *OAuthServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
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" {
|
if r.URL.Path == "/token" {
|
||||||
fmt.Fprintf(w, `{"access_token":"123456789"}`)
|
fmt.Fprintf(w, `{"access_token":"123456789"}`)
|
||||||
} else if r.URL.Path == "/userinfo" {
|
} else if r.URL.Path == "/userinfo" {
|
||||||
@ -368,6 +520,13 @@ func NewOAuthServer(t *testing.T) (*httptest.Server, *url.URL) {
|
|||||||
return server, serverURL
|
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) {
|
func doHttpRequest(r *http.Request, c *http.Cookie) (*http.Response, string) {
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
@ -405,16 +564,17 @@ func newDefaultConfig() *Config {
|
|||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: replace with newHTTPRequest("GET", "http://example.com/"+uri)
|
||||||
func newDefaultHttpRequest(uri string) *http.Request {
|
func newDefaultHttpRequest(uri string) *http.Request {
|
||||||
return newHttpRequest("", "http://example.com/", uri)
|
return newHTTPRequest("GET", "http://example.com"+uri)
|
||||||
}
|
}
|
||||||
|
|
||||||
func newHttpRequest(method, dest, uri string) *http.Request {
|
func newHTTPRequest(method, target string) *http.Request {
|
||||||
r := httptest.NewRequest("", "http://should-use-x-forwarded.com", nil)
|
u, _ := url.Parse(target)
|
||||||
p, _ := url.Parse(dest)
|
r := httptest.NewRequest(method, target, nil)
|
||||||
r.Header.Add("X-Forwarded-Method", method)
|
r.Header.Add("X-Forwarded-Method", method)
|
||||||
r.Header.Add("X-Forwarded-Proto", p.Scheme)
|
r.Header.Add("X-Forwarded-Proto", u.Scheme)
|
||||||
r.Header.Add("X-Forwarded-Host", p.Host)
|
r.Header.Add("X-Forwarded-Host", u.Host)
|
||||||
r.Header.Add("X-Forwarded-Uri", uri)
|
r.Header.Add("X-Forwarded-Uri", u.RequestURI())
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user