Compare commits
7 Commits
v2.0.0-rc1
...
v2.0.0-rc3
Author | SHA1 | Date | |
---|---|---|---|
5a17187855 | |||
e7b567bc92 | |||
a4a34dcd76 | |||
d1b12e4ffb | |||
6f3ac5efe5 | |||
b0e4b6333d | |||
dd13f42ddf |
336
README.md
336
README.md
@ -1,90 +1,308 @@
|
||||
|
||||
# Traefik Forward Auth [](https://travis-ci.org/thomseddon/traefik-forward-auth) [](https://goreportcard.com/badge/github.com/thomseddon/traefik-forward-auth)
|
||||
# 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/)
|
||||
|
||||
A minimal forward authentication service that provides Google oauth based login and authentication for the traefik reverse proxy.
|
||||
|
||||
A minimal forward authentication service that provides Google oauth based login and authentication for the [traefik](https://github.com/containous/traefik) reverse proxy/load balancer.
|
||||
|
||||
|
||||
## Why?
|
||||
|
||||
- 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 domains/subdomains by dynamically generating redirect_uri's
|
||||
- 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)))
|
||||
- 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))
|
||||
|
||||
## Quick Start
|
||||
# Contents
|
||||
|
||||
See the (examples) directory for example docker compose and traefik configuration files that demonstrates the forward authentication configuration for traefik and passing required configuration values to traefik-forward-auth.
|
||||
|
||||
## Configuration
|
||||
|
||||
The following configuration is supported:
|
||||
|
||||
|
||||
|Flag |Type |Description|
|
||||
|-----------------------|------|-----------|
|
||||
|-client-id|string|*Google Client ID (required)|
|
||||
|-client-secret|string|*Google Client Secret (required)|
|
||||
|-secret|string|*Secret used for signing (required)|
|
||||
|-config|string|Path to config file|
|
||||
|-auth-host|string|Central auth login (see below)|
|
||||
|-cookie-domains|string|Comma separated list of cookie domains (see below)|
|
||||
|-cookie-name|string|Cookie Name (default "_forward_auth")|
|
||||
|-cookie-secure|bool|Use secure cookies (default true)|
|
||||
|-csrf-cookie-name|string|CSRF Cookie Name (default "_forward_auth_csrf")|
|
||||
|-domain|string|Comma separated list of email domains to allow|
|
||||
|-whitelist|string|Comma separated list of email addresses to allow|
|
||||
|-lifetime|int|Session length in seconds (default 43200)|
|
||||
|-url-path|string|Callback URL (default "_oauth")|
|
||||
|-prompt|string|Space separated list of [OpenID prompt options](https://developers.google.com/identity/protocols/OpenIDConnect#prompt)|
|
||||
|-log-level|string|Log level: trace, debug, info, warn, error, fatal, panic (default "warn")|
|
||||
|-log-format|string|Log format: text, json, pretty (default "text")|
|
||||
|
||||
Configuration can also be supplied as environment variables (use upper case and swap `-`'s for `_`'s e.g. `-client-id` becomes `CLIENT_ID`)
|
||||
|
||||
Configuration can also be supplied via a file, you can specify the location with `-config` flag, the format is `flag value` one per line, e.g. `client-id your-client-id`)
|
||||
|
||||
## OAuth Configuration
|
||||
|
||||
Head to https://console.developers.google.com & make sure you've switched to the correct email account.
|
||||
|
||||
Create a new project then search for and select "Credentials" in the search bar. Fill out the "OAuth Consent Screen" tab.
|
||||
|
||||
Click, "Create Credentials" > "OAuth client ID". Select "Web Application", fill in the name of your app, skip "Authorized JavaScript origins" and fill "Authorized redirect URIs" with all the domains you will allow authentication from, appended with the `url-path` (e.g. https://app.test.com/_oauth)
|
||||
- [Usage](#usage)
|
||||
- [Simple](#simple)
|
||||
- [Advanced](#advanced)
|
||||
- [OAuth Configuration](#oauth-configuration)
|
||||
- [Configuration](#configuration)
|
||||
- [Overview](#overview)
|
||||
- [Option Details](#option-details)
|
||||
- [Concepts](#concepts)
|
||||
- [Forwarded Headers](#forwarded-headers)
|
||||
- [User Restriction](#user-restriction)
|
||||
- [Operation Modes](#operation-modes)
|
||||
- [Overlay Mode](#overlay-mode)
|
||||
- [Auth Host Mode](#auth-host-mode)
|
||||
- [Copyright](#copyright)
|
||||
- [License](#license)
|
||||
|
||||
## Usage
|
||||
|
||||
The authenticated user is set in the `X-Forwarded-User` header, to pass this on add this to the `authResponseHeaders` as shown [here](https://github.com/thomseddon/traefik-forward-auth/blob/master/example/docker-compose-dev.yml).
|
||||
#### Simple:
|
||||
|
||||
## User Restriction
|
||||
See below for instructions on how to setup your [OAuth Configuration](#oauth-configuration).
|
||||
|
||||
docker-compose.yml:
|
||||
|
||||
```yaml
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
traefik:
|
||||
image: traefik:1.7
|
||||
ports:
|
||||
- "8085:80"
|
||||
volumes:
|
||||
- ./traefik.toml:/traefik.toml
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
|
||||
traefik-forward-auth:
|
||||
image: thomseddon/traefik-forward-auth:2
|
||||
environment:
|
||||
- CLIENT_ID=your-client-id
|
||||
- CLIENT_SECRET=your-client-secret
|
||||
- SECRET=something-random
|
||||
- INSECURE_COOKIE=true # Example assumes no https, do not use in production
|
||||
|
||||
whoami:
|
||||
image: emilevauge/whoami:latest
|
||||
labels:
|
||||
- "traefik.frontend.rule=Host:whoami.mycompany.com"
|
||||
```
|
||||
|
||||
traefik.toml:
|
||||
|
||||
```toml
|
||||
[entryPoints]
|
||||
[entryPoints.http]
|
||||
address = ":80"
|
||||
|
||||
[entryPoints.http.auth.forward]
|
||||
address = "http://traefik-forward-auth:4181"
|
||||
authResponseHeaders = ["X-Forwarded-User"]
|
||||
|
||||
[docker]
|
||||
endpoint = "unix:///var/run/docker.sock"
|
||||
network = "traefik"
|
||||
```
|
||||
|
||||
#### 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).
|
||||
|
||||
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.
|
||||
|
||||
#### OAuth Configuration
|
||||
|
||||
Head to https://console.developers.google.com and make sure you've switched to the correct email account.
|
||||
|
||||
Create a new project then search for and select "Credentials" in the search bar. Fill out the "OAuth Consent Screen" tab.
|
||||
|
||||
Click "Create Credentials" > "OAuth client ID". Select "Web Application", fill in the name of your app, skip "Authorized JavaScript origins" and fill "Authorized redirect URIs" with all the domains you will allow authentication from, appended with the `url-path` (e.g. https://app.test.com/_oauth)
|
||||
|
||||
#### Upgrade Guide
|
||||
|
||||
v2 was released in April 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.
|
||||
|
||||
## Configuration
|
||||
|
||||
### Overview
|
||||
|
||||
The following configuration options are supported:
|
||||
|
||||
```
|
||||
Usage:
|
||||
traefik-forward-auth [OPTIONS]
|
||||
|
||||
Application Options:
|
||||
--log-level=[trace|debug|info|warn|error|fatal|panic] Log level (default: warn) [$LOG_LEVEL]
|
||||
--log-format=[text|json|pretty] Log format (default: text) [$LOG_FORMAT]
|
||||
--auth-host= Single host to use when returning from 3rd party auth [$AUTH_HOST]
|
||||
--config= Path to config file [$CONFIG]
|
||||
--cookie-domain= Domain to set auth cookie on, can be set multiple times [$COOKIE_DOMAIN]
|
||||
--insecure-cookie Use insecure cookies [$INSECURE_COOKIE]
|
||||
--cookie-name= Cookie Name (default: _forward_auth) [$COOKIE_NAME]
|
||||
--csrf-cookie-name= CSRF Cookie Name (default: _forward_auth_csrf) [$CSRF_COOKIE_NAME]
|
||||
--default-action=[auth|allow] Default action (default: auth) [$DEFAULT_ACTION]
|
||||
--domain= Only allow given email domains, can be set multiple times [$DOMAIN]
|
||||
--lifetime= Lifetime in seconds (default: 43200) [$LIFETIME]
|
||||
--url-path= Callback URL Path (default: /_oauth) [$URL_PATH]
|
||||
--secret= Secret used for signing (required) [$SECRET]
|
||||
--whitelist= Only allow given email addresses, can be set multiple times [$WHITELIST]
|
||||
--rules.<name>.<param>= Rule definitions, param can be: "action" or "rule"
|
||||
|
||||
Google Provider:
|
||||
--providers.google.client-id= Client ID [$PROVIDERS_GOOGLE_CLIENT_ID]
|
||||
--providers.google.client-secret= Client Secret [$PROVIDERS_GOOGLE_CLIENT_SECRET]
|
||||
--providers.google.prompt= Space separated list of OpenID prompt options [$PROVIDERS_GOOGLE_PROMPT]
|
||||
|
||||
Help Options:
|
||||
-h, --help Show this help message
|
||||
```
|
||||
|
||||
All options can be supplied in any of the following ways, in the following precedence (first is highest precedence):
|
||||
|
||||
1. **Command Arguments/Flags** - As shown above
|
||||
2. **Environment Variables** - As shown in square brackets above
|
||||
3. **File**
|
||||
1. Use INI format (e.g. `url-path = _oauthpath`)
|
||||
2. Specify the file location via the `--config` flag or `$CONFIG` environment variable
|
||||
3. Can be specified multiple times, each file will be read in the order they are passed
|
||||
|
||||
### Option Details
|
||||
|
||||
- `auth-host`
|
||||
|
||||
When set, when a user returns from authentication with a 3rd party provider they will always be forwarded to this host. By using one central host, this means you only need to add this `auth-host` as a valid redirect uri to your 3rd party provider.
|
||||
|
||||
The host should be specified without protocol or path, for example:
|
||||
|
||||
```
|
||||
--auth-host="auth.example.com"
|
||||
```
|
||||
|
||||
For more details, please also read the [Auth Host Mode](#auth-host-mode), operation mode in the concepts section.
|
||||
|
||||
Please Note - this should be considered advanced usage, if you are having problems please try disabling this option and then re-read the [Auth Host Mode](#auth-host-mode) section.
|
||||
|
||||
- `config`
|
||||
|
||||
Used to specify the path to a configuration file, can be set multiple times, each file will be read in the order they are passed. Options should be set in an INI format, for example:
|
||||
|
||||
```
|
||||
url-path = _oauthpath
|
||||
```
|
||||
|
||||
- `cookie-domain`
|
||||
|
||||
When set, if a user successfully completes authentication, then if the host of the original request requiring authentication is a subdomain of a given cookie domain, then the authentication cookie will be set for the higher level cookie domain. This means that a cookie can allow access to multiple subdomains without re-authentication. Can be specificed multiple times.
|
||||
|
||||
For example:
|
||||
```
|
||||
--cookie-domain="example.com" --cookie-domain="test.org"
|
||||
```
|
||||
|
||||
For example, if the cookie domain `test.com` has been set, and a request comes in on `app1.test.com`, following authentication the auth cookie will be set for the whole `test.com` domain. As such, if another request is forwarded for authentication from `app2.test.com`, the original cookie will be sent and so the request will be allowed without further authentication.
|
||||
|
||||
Beware however, if using cookie domains whilst running multiple instances of traefik/traefik-forward-auth for the same domain, the cookies will clash. You can fix this by using a different `cookie-name` in each host/cluster or by using the same `cookie-secret` in both instances.
|
||||
|
||||
- `insecure-cookie`
|
||||
|
||||
If you are not using HTTPS between the client and traefik, you will need to pass the `insecure-cookie` option which will mean the `Secure` attribute on the cookie will not be set.
|
||||
|
||||
- `cookie-name`
|
||||
|
||||
Set the name of the cookie set following successful authentication.
|
||||
|
||||
Default: `_forward_auth`
|
||||
|
||||
- `csrf-cookie-name`
|
||||
|
||||
Set the name of the temporary CSRF cookie set during authentication.
|
||||
|
||||
Default: `_forward_auth_csrf`
|
||||
|
||||
- `default-action`
|
||||
|
||||
Specifies the behavior when a request does not match any [rules](#rules). Valid options are `auth` or `allow`.
|
||||
|
||||
Default: `auth` (i.e. all requests require authentication)
|
||||
|
||||
- `domain`
|
||||
|
||||
When set, only users matching a given domain will be permitted to access.
|
||||
|
||||
For example, setting `--domain=example.com --domain=test.org` would mean that only users from example.com or test.org will be permitted. So thom@example.com would be allowed but thom@another.com would not.
|
||||
|
||||
For more details, please also read [User Restriction](#user-restriction) in the concepts section.
|
||||
|
||||
- `lifetime`
|
||||
|
||||
How long a successful authentication session should last, in seconds.
|
||||
|
||||
Default: `43200` (12 hours)
|
||||
|
||||
- `url-path`
|
||||
|
||||
Customise the path that this service uses to handle the callback following authentication.
|
||||
|
||||
Default: `/_oauth`
|
||||
|
||||
Please note that when using the default [Overlay Mode](#overlay-mode) requests to this exact path will be intercepted by this service and not forwarded to your application. Use this option (or [Auth Host Mode](#auth-host-mode)) if the default `/_oauth` path will collide with an existing route in your application.
|
||||
|
||||
- `secret`
|
||||
|
||||
Used to sign cookies authentication, should be a random (e.g. `openssl rand -hex 16`)
|
||||
|
||||
- `whitelist`
|
||||
|
||||
When set, only specified users will be permitted.
|
||||
|
||||
For example, setting `--whitelist=thom@example.com --whitelist=alice@example.com` would mean that only those two exact users will be permitted. So thom@example.com would be allowed but john@example.com would not.
|
||||
|
||||
For more details, please also read [User Restriction](#user-restriction) in the concepts section.
|
||||
|
||||
- `rules`
|
||||
|
||||
Specify selective authentication rules. Rules are specified in the following format: `rule.<name>.<param>=<value>`
|
||||
|
||||
- `<name>` can be any string and is only used to group rules together
|
||||
- `<param>` can be:
|
||||
- `action` - same usage as [`default-action`](#default-action), supported values:
|
||||
- `auth` (default)
|
||||
- `allow`
|
||||
- `rule` - a rule to match a request, this uses traefik's v2 rule parser for which you can find the documentation here: https://docs.traefik.io/v2.0/routing/routers/#rule, supported values are summarised here:
|
||||
- ``Headers(`key`, `value`)``
|
||||
- ``HeadersRegexp(`key`, `regexp`)``
|
||||
- ``Host(`example.com`, ...)``
|
||||
- ``HostRegexp(`example.com`, `{subdomain:[a-z]+}.example.com`, ...)``
|
||||
- ``Method(methods, ...)``
|
||||
- ``Path(`path`, `/articles/{category}/{id:[0-9]+}`, ...)``
|
||||
- ``PathPrefix(`/products/`, `/articles/{category}/{id:[0-9]+}`)``
|
||||
- ``Query(`foo=bar`, `bar=baz`)``
|
||||
|
||||
For example:
|
||||
```
|
||||
rule.1.action = allow
|
||||
rule.1.rule = PathPrefix(`/api/public`) && Headers(`Content-Type`, `application/json`)
|
||||
|
||||
rule.two.action = allow
|
||||
rule.two.rule = Path(`/public`)
|
||||
```
|
||||
|
||||
In the above example, the first rule would allow requests that begin with `/api/public` and contain the `Content-Type` header with a value of `application/json`. It would also allow requests that had the exact path `/public`.
|
||||
|
||||
## Concepts
|
||||
|
||||
### User Restriction
|
||||
|
||||
You can restrict who can login with the following parameters:
|
||||
|
||||
* `-domain` - Use this to limit logins to a specific domain, e.g. test.com only
|
||||
* `-whitelist` - Use this to only allow specific users to login e.g. thom@test.com only
|
||||
* `domain` - Use this to limit logins to a specific domain, e.g. test.com only
|
||||
* `whitelist` - Use this to only allow specific users to login e.g. thom@test.com only
|
||||
|
||||
Note, if you pass `whitelist` then only this is checked and `domain` is effectively ignored.
|
||||
|
||||
## Cookie Domains
|
||||
### Forwarded Headers
|
||||
|
||||
You can supply a comma separated list of cookie domains, if the host of the original request is a subdomain of any given cookie domain, the authentication cookie will set with the given domain.
|
||||
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).
|
||||
|
||||
For example, if cookie domain is `test.com` and a request comes in on `app1.test.com`, the cookie will be set for the whole `test.com` domain. As such, if another request is forwarded for authentication from `app2.test.com`, the original cookie will be sent and so the request will be allowed without further authentication.
|
||||
### Operation Modes
|
||||
|
||||
Beware however, if using cookie domains whilst running multiple instances of traefik/traefik-forward-auth for the same domain, the cookies will clash. You can fix this by using the same `cookie-secret` in both instances, or using a different `cookie-name` on each.
|
||||
#### Overlay Mode
|
||||
|
||||
## Operation Modes
|
||||
Overlay is the default operation mode, in this mode the authorisation endpoint is overlayed onto any domain. By default the `/_oauth` path is used, this can be customised using the `url-path` option.
|
||||
|
||||
#### Overlay
|
||||
The user flow will be:
|
||||
|
||||
Overlay is the default operation mode, in this mode the authorisation endpoint is overlayed onto any domain. By default the `/_oauth` path is used, this can be customised using the `-url-path` option.
|
||||
1. Request to `www.myapp.com/home`
|
||||
2. User redirected to Google login
|
||||
3. After Google login, user is redirected to `www.myapp.com/_oauth`
|
||||
4. Token, user and CSRF cookie is validated (this request in intercepted and is never passed to your application)
|
||||
5. User is redirected to `www.myapp.com/home`
|
||||
6. Request is allowed
|
||||
|
||||
If a request comes in for `www.myapp.com/home` then the user will be redirected to the google login, following this they will be sent back to `www.myapp.com/_oauth`, where their token will be validated (this request will not be forwarded to your application). Following successful authoristion, the user will return to their originally requested url of `www.myapp.com/home`.
|
||||
As the hostname in the `redirect_uri` is dynamically generated based on the original request, every hostname must be permitted in the Google OAuth console (e.g. `www.myappp.com` would need to be added in the above example)
|
||||
|
||||
As the hostname in the `redirect_uri` is dynamically generated based on the orignal request, every hostname must be permitted in the Google OAuth console (e.g. `www.myappp.com` would need to be added in the above example)
|
||||
#### Auth Host Mode
|
||||
|
||||
#### Auth Host
|
||||
|
||||
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/example/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/docker-compose-auth-host.yml)).
|
||||
|
||||
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`.
|
||||
@ -105,6 +323,8 @@ Two criteria must be met for an `auth-host` to be used:
|
||||
1. Request matches given `cookie-domain`
|
||||
2. `auth-host` is also subdomain of same `cookie-domain`
|
||||
|
||||
Please note: For Auth Host mode to work, you must ensure that requests to your auth-host are routed to the traefik-forward-auth container, as demonstrated with the service labels in the [docker-compose-auth.yml](https://github.com/thomseddon/traefik-forward-auth/blob/master/examples/docker-compose-auth-host.yml) example.
|
||||
|
||||
## Copyright
|
||||
|
||||
2018 Thom Seddon
|
||||
|
@ -25,15 +25,15 @@ services:
|
||||
traefik-forward-auth:
|
||||
image: thomseddon/traefik-forward-auth
|
||||
environment:
|
||||
- CLIENT_ID=your-client-id
|
||||
- CLIENT_SECRET=your-client-secret
|
||||
- PROVIDERS_GOOGLE_CLIENT_ID=your-client-id
|
||||
- PROVIDERS_GOOGLE_CLIENT_SECRET=your-client-secret
|
||||
- SECRET=something-random
|
||||
- COOKIE_SECURE=false
|
||||
- INSECURE_COOKIE=true
|
||||
- DOMAIN=yourcompany.com
|
||||
- AUTH_HOST=auth.yourdomain.com
|
||||
networks:
|
||||
- traefik
|
||||
# When using an auth host, adding it here prompts traefik to generate certs
|
||||
# When using an auth host, the below must be added
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
- traefik.port=4181
|
@ -35,12 +35,12 @@ services:
|
||||
traefik-forward-auth:
|
||||
build: ../
|
||||
environment:
|
||||
- CLIENT_ID=test
|
||||
- CLIENT_SECRET=test
|
||||
- COOKIE_SECRET=something-random
|
||||
- COOKIE_SECURE=false
|
||||
- COOKIE_DOMAINS=localhost.com
|
||||
- AUTH_URL=http://auth.localhost.com:8085/_oauth
|
||||
- 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
|
||||
|
@ -23,13 +23,15 @@ services:
|
||||
- "traefik.frontend.rule=Host:whoami.localhost.com"
|
||||
|
||||
traefik-forward-auth:
|
||||
image: thomseddon/traefik-forward-auth
|
||||
build: ../
|
||||
command: ./traefik-forward-auth --rule.1.action=allow --rule.1.rule="Path(`/`)"
|
||||
environment:
|
||||
- CLIENT_ID=your-client-id
|
||||
- CLIENT_SECRET=your-client-secret
|
||||
- PROVIDERS_GOOGLE_CLIENT_ID=your-client-id
|
||||
- PROVIDERS_GOOGLE_CLIENT_SECRET=your-client-secret
|
||||
- SECRET=something-random
|
||||
- COOKIE_SECURE=false
|
||||
- INSECURE_COOKIE=true
|
||||
- DOMAIN=yourcompany.com
|
||||
- LOG_LEVEL=debug
|
||||
networks:
|
||||
- traefik
|
||||
|
3
go.mod
3
go.mod
@ -13,7 +13,7 @@ require (
|
||||
github.com/go-kit/kit v0.8.0 // indirect
|
||||
github.com/gorilla/context v1.1.1 // indirect
|
||||
github.com/gravitational/trace v0.0.0-20190409171327-f30095ced5ff // indirect
|
||||
github.com/jessevdk/go-flags v1.4.0
|
||||
github.com/jessevdk/go-flags v1.4.1-0.20181221193153-c0795c8afcf4
|
||||
github.com/jonboulle/clockwork v0.1.0 // indirect
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
|
||||
github.com/kr/pretty v0.1.0 // indirect
|
||||
@ -25,6 +25,7 @@ require (
|
||||
github.com/sirupsen/logrus v1.4.1
|
||||
github.com/stretchr/objx v0.2.0 // indirect
|
||||
github.com/stretchr/testify v1.3.0
|
||||
github.com/thomseddon/go-flags v1.4.1-0.20190507184247-a3629c504486
|
||||
github.com/vulcand/predicate v1.1.0 // indirect
|
||||
golang.org/x/crypto v0.0.0-20190422183909-d864b10871cd // indirect
|
||||
golang.org/x/net v0.0.0-20190420063019-afa5a82059c6 // indirect
|
||||
|
8
go.sum
8
go.sum
@ -24,6 +24,8 @@ github.com/gravitational/trace v0.0.0-20190409171327-f30095ced5ff h1:xL/fJdlTJL6
|
||||
github.com/gravitational/trace v0.0.0-20190409171327-f30095ced5ff/go.mod h1:RvdOUHE4SHqR3oXlFFKnGzms8a5dugHygGw1bqDstYI=
|
||||
github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/jessevdk/go-flags v1.4.1-0.20181221193153-c0795c8afcf4 h1:xKkUL6QBojwguhKKetf1SocCAKqc6W7S/mGm9xEGllo=
|
||||
github.com/jessevdk/go-flags v1.4.1-0.20181221193153-c0795c8afcf4/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/jonboulle/clockwork v0.1.0 h1:VKV+ZcuP6l3yW9doeqz6ziZGgcynBVQO+obU0+0hcPo=
|
||||
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
|
||||
@ -53,6 +55,12 @@ github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoH
|
||||
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/thomseddon/go-flags v1.4.0 h1:cHj56pbnQxlGo2lx2P8f0Dph4TRYKBJzoPuF2lqNvW4=
|
||||
github.com/thomseddon/go-flags v1.4.0/go.mod h1:NK9eZpNBmSKVxvyB/MExg6jW0Bo9hQyAuCP+b8MJFow=
|
||||
github.com/thomseddon/go-flags v1.4.1-0.20190507181358-ce437f05b7fb h1:L311/fJ7WXmFDDtuhf22PkVJqZpqLbEsmGSTEGv7ZQY=
|
||||
github.com/thomseddon/go-flags v1.4.1-0.20190507181358-ce437f05b7fb/go.mod h1:NK9eZpNBmSKVxvyB/MExg6jW0Bo9hQyAuCP+b8MJFow=
|
||||
github.com/thomseddon/go-flags v1.4.1-0.20190507184247-a3629c504486 h1:hk17f4niAl4e6viTj2uf/fpfACa6QPmrtMDAo+1tifE=
|
||||
github.com/thomseddon/go-flags v1.4.1-0.20190507184247-a3629c504486/go.mod h1:NK9eZpNBmSKVxvyB/MExg6jW0Bo9hQyAuCP+b8MJFow=
|
||||
github.com/vulcand/predicate v1.1.0 h1:Gq/uWopa4rx/tnZu2opOSBqHK63Yqlou/SzrbwdJiNg=
|
||||
github.com/vulcand/predicate v1.1.0/go.mod h1:mlccC5IRBoc2cIFmCB8ZM62I3VDb6p2GXESMHa3CnZg=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
|
@ -14,7 +14,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/jessevdk/go-flags"
|
||||
"github.com/thomseddon/go-flags"
|
||||
"github.com/thomseddon/traefik-forward-auth/internal/provider"
|
||||
)
|
||||
|
||||
@ -25,7 +25,7 @@ type Config struct {
|
||||
LogFormat string `long:"log-format" env:"LOG_FORMAT" default:"text" choice:"text" choice:"json" choice:"pretty" description:"Log format"`
|
||||
|
||||
AuthHost string `long:"auth-host" env:"AUTH_HOST" description:"Single host to use when returning from 3rd party auth"`
|
||||
Config func(s string) error `long:"config" env:"CONFIG" description:"Path to config file"`
|
||||
Config func(s string) error `long:"config" env:"CONFIG" description:"Path to config file" json:"-"`
|
||||
CookieDomains []CookieDomain `long:"cookie-domain" env:"COOKIE_DOMAIN" description:"Domain to set auth cookie on, can be set multiple times"`
|
||||
InsecureCookie bool `long:"insecure-cookie" env:"INSECURE_COOKIE" description:"Use insecure cookies"`
|
||||
CookieName string `long:"cookie-name" env:"COOKIE_NAME" default:"_forward_auth" description:"Cookie Name"`
|
||||
@ -34,23 +34,23 @@ type Config struct {
|
||||
Domains []string `long:"domain" env:"DOMAIN" description:"Only allow given email domains, can be set multiple times"`
|
||||
LifetimeString int `long:"lifetime" env:"LIFETIME" default:"43200" description:"Lifetime in seconds"`
|
||||
Path string `long:"url-path" env:"URL_PATH" default:"/_oauth" description:"Callback URL Path"`
|
||||
SecretString string `long:"secret" env:"SECRET" description:"Secret used for signing (required)"`
|
||||
SecretString string `long:"secret" env:"SECRET" description:"Secret used for signing (required)" json:"-"`
|
||||
Whitelist CommaSeparatedList `long:"whitelist" env:"WHITELIST" description:"Only allow given email addresses, can be set multiple times"`
|
||||
|
||||
Providers provider.Providers `group:"providers" namespace:"providers" env-namespace:"PROVIDERS"`
|
||||
Rules map[string]*Rule `long:"rules.<name>.<param>" description:"Rule definitions, param can be: \"action\" or \"rule\""`
|
||||
|
||||
// Filled during transformations
|
||||
Secret []byte
|
||||
Secret []byte `json:"-"`
|
||||
Lifetime time.Duration
|
||||
|
||||
// Legacy
|
||||
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\""`
|
||||
CookieSecretLegacy string `long:"cookie-secret" env:"COOKIE_SECRET" description:"DEPRECATED - Use \"secret\"" json:"-"`
|
||||
CookieSecureLegacy string `long:"cookie-secure" env:"COOKIE_SECURE" description:"DEPRECATED - Use \"insecure-cookie\""`
|
||||
DomainsLegacy CommaSeparatedList `long:"domains" env:"DOMAINS" description:"DEPRECATED - Use \"domain\""`
|
||||
ClientIdLegacy string `long:"client-id" env:"CLIENT_ID" group:"DEPs" description:"DEPRECATED - Use \"providers.google.client-id\""`
|
||||
ClientSecretLegacy string `long:"client-secret" env:"CLIENT_SECRET" description:"DEPRECATED - Use \"providers.google.client-id\""`
|
||||
ClientSecretLegacy string `long:"client-secret" env:"CLIENT_SECRET" description:"DEPRECATED - Use \"providers.google.client-id\"" json:"-"`
|
||||
PromptLegacy string `long:"prompt" env:"PROMPT" description:"DEPRECATED - Use \"providers.google.prompt\""`
|
||||
}
|
||||
|
||||
@ -100,6 +100,7 @@ func NewConfig(args []string) (Config, error) {
|
||||
|
||||
// Backwards compatability
|
||||
if c.CookieSecretLegacy != "" && c.SecretString == "" {
|
||||
fmt.Println("cookie-secret config option is deprecated, please use secret")
|
||||
c.SecretString = c.CookieSecretLegacy
|
||||
}
|
||||
if c.ClientIdLegacy != "" {
|
||||
@ -109,9 +110,11 @@ func NewConfig(args []string) (Config, error) {
|
||||
c.Providers.Google.ClientSecret = c.ClientSecretLegacy
|
||||
}
|
||||
if c.PromptLegacy != "" {
|
||||
fmt.Println("prompt config option is deprecated, please use providers.google.prompt")
|
||||
c.Providers.Google.Prompt = c.PromptLegacy
|
||||
}
|
||||
if c.CookieSecureLegacy != "" {
|
||||
fmt.Println("cookie-secure config option is deprecated, please use insecure-cookie")
|
||||
secure, err := strconv.ParseBool(c.CookieSecureLegacy)
|
||||
if err != nil {
|
||||
return c, err
|
||||
@ -119,9 +122,11 @@ func NewConfig(args []string) (Config, error) {
|
||||
c.InsecureCookie = !secure
|
||||
}
|
||||
if len(c.CookieDomainsLegacy) > 0 {
|
||||
fmt.Println("cookie-domains config option is deprecated, please use cookie-domain")
|
||||
c.CookieDomains = append(c.CookieDomains, c.CookieDomainsLegacy...)
|
||||
}
|
||||
if len(c.DomainsLegacy) > 0 {
|
||||
fmt.Println("domains config option is deprecated, please use domain")
|
||||
c.Domains = append(c.Domains, c.DomainsLegacy...)
|
||||
}
|
||||
|
||||
@ -136,7 +141,7 @@ func NewConfig(args []string) (Config, error) {
|
||||
}
|
||||
|
||||
func (c *Config) parseFlags(args []string) error {
|
||||
p := flags.NewParser(c, flags.Default)
|
||||
p := flags.NewParser(c, flags.Default|flags.IniUnknownOptionHandler)
|
||||
p.UnknownOptionHandler = c.parseUnknownFlag
|
||||
|
||||
i := flags.NewIniParser(p)
|
||||
@ -152,6 +157,7 @@ func (c *Config) parseFlags(args []string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println("config format deprecated, please use ini format")
|
||||
return i.Parse(converted)
|
||||
}
|
||||
|
||||
@ -271,6 +277,12 @@ func NewRule() *Rule {
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Rule) formattedRule() string {
|
||||
// Traefik implements their own "Host" matcher and then offers "HostRegexp"
|
||||
// to invoke the mux "Host" matcher. This ensures the mux version is used
|
||||
return strings.ReplaceAll(r.Rule, "Host(", "HostRegexp(")
|
||||
}
|
||||
|
||||
func (r *Rule) Validate() {
|
||||
if r.Action != "auth" && r.Action != "allow" {
|
||||
log.Fatal("invalid rule action, must be \"auth\" or \"allow\"")
|
||||
|
@ -165,6 +165,18 @@ func TestConfigParseIni(t *testing.T) {
|
||||
assert.Equal("inicookiename", c.CookieName, "should be read from ini file")
|
||||
assert.Equal("csrfcookiename", c.CSRFCookieName, "should be read from ini file")
|
||||
assert.Equal("/two", c.Path, "variable in second ini file should override first ini file")
|
||||
assert.Equal(map[string]*Rule{
|
||||
"1": {
|
||||
Action: "allow",
|
||||
Rule: "PathPrefix(`/one`)",
|
||||
Provider: "google",
|
||||
},
|
||||
"two": {
|
||||
Action: "auth",
|
||||
Rule: "Host(`two.com`) && Path(`/two`)",
|
||||
Provider: "google",
|
||||
},
|
||||
}, c.Rules)
|
||||
}
|
||||
|
||||
func TestConfigFileBackwardsCompatability(t *testing.T) {
|
||||
@ -181,10 +193,12 @@ func TestConfigFileBackwardsCompatability(t *testing.T) {
|
||||
func TestConfigParseEnvironment(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
os.Setenv("COOKIE_NAME", "env_cookie_name")
|
||||
os.Setenv("PROVIDERS_GOOGLE_CLIENT_ID", "env_client_id")
|
||||
c, err := NewConfig([]string{})
|
||||
assert.Nil(err)
|
||||
|
||||
assert.Equal("env_cookie_name", c.CookieName, "variable should be read from environment")
|
||||
assert.Equal("env_client_id", c.Providers.Google.ClientId, "namespace variable should be read from environment")
|
||||
}
|
||||
|
||||
func TestConfigTransformation(t *testing.T) {
|
||||
|
@ -26,11 +26,11 @@ func (s *Server) buildRoutes() {
|
||||
}
|
||||
|
||||
// Let's build a router
|
||||
for _, rule := range config.Rules {
|
||||
for name, rule := range config.Rules {
|
||||
if rule.Action == "allow" {
|
||||
s.router.AddRoute(rule.Rule, 1, s.AllowHandler())
|
||||
s.router.AddRoute(rule.formattedRule(), 1, s.AllowHandler(name))
|
||||
} else {
|
||||
s.router.AddRoute(rule.Rule, 1, s.AuthHandler())
|
||||
s.router.AddRoute(rule.formattedRule(), 1, s.AuthHandler(name))
|
||||
}
|
||||
}
|
||||
|
||||
@ -39,14 +39,16 @@ func (s *Server) buildRoutes() {
|
||||
|
||||
// Add a default handler
|
||||
if config.DefaultAction == "allow" {
|
||||
s.router.NewRoute().Handler(s.AllowHandler())
|
||||
s.router.NewRoute().Handler(s.AllowHandler("default"))
|
||||
} else {
|
||||
s.router.NewRoute().Handler(s.AuthHandler())
|
||||
s.router.NewRoute().Handler(s.AuthHandler("default"))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) RootHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// Modify request
|
||||
r.Method = r.Header.Get("X-Forwarded-Method")
|
||||
r.Host = r.Header.Get("X-Forwarded-Host")
|
||||
r.URL, _ = url.Parse(r.Header.Get("X-Forwarded-Uri"))
|
||||
|
||||
// Pass to mux
|
||||
@ -54,18 +56,18 @@ func (s *Server) RootHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// Handler that allows requests
|
||||
func (s *Server) AllowHandler() http.HandlerFunc {
|
||||
func (s *Server) AllowHandler(rule string) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
s.logger(r, "Allowing request")
|
||||
s.logger(r, rule, "Allowing request")
|
||||
w.WriteHeader(200)
|
||||
}
|
||||
}
|
||||
|
||||
// Authenticate requests
|
||||
func (s *Server) AuthHandler() http.HandlerFunc {
|
||||
func (s *Server) AuthHandler(rule string) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
// Logging setup
|
||||
logger := s.logger(r, "Authenticating request")
|
||||
logger := s.logger(r, rule, "Authenticating request")
|
||||
|
||||
// Get auth cookie
|
||||
c, err := r.Cookie(config.CookieName)
|
||||
@ -118,7 +120,7 @@ func (s *Server) AuthHandler() http.HandlerFunc {
|
||||
func (s *Server) AuthCallbackHandler() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
// Logging setup
|
||||
logger := s.logger(r, "Handling callback")
|
||||
logger := s.logger(r, "default", "Handling callback")
|
||||
|
||||
// Check for CSRF cookie
|
||||
c, err := r.Cookie(config.CSRFCookieName)
|
||||
@ -165,16 +167,17 @@ func (s *Server) AuthCallbackHandler() http.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) logger(r *http.Request, msg string) *logrus.Entry {
|
||||
func (s *Server) logger(r *http.Request, rule, msg string) *logrus.Entry {
|
||||
// Create logger
|
||||
logger := log.WithFields(logrus.Fields{
|
||||
"SourceIP": r.Header.Get("X-Forwarded-For"),
|
||||
"source_ip": r.Header.Get("X-Forwarded-For"),
|
||||
})
|
||||
|
||||
// Log request
|
||||
logger.WithFields(logrus.Fields{
|
||||
"Headers": r.Header,
|
||||
}).Debugf(msg)
|
||||
"rule": rule,
|
||||
"headers": r.Header,
|
||||
}).Debug(msg)
|
||||
|
||||
return logger
|
||||
}
|
||||
|
@ -32,7 +32,7 @@ func TestServerAuthHandler(t *testing.T) {
|
||||
config, _ = NewConfig([]string{})
|
||||
|
||||
// Should redirect vanilla request to login url
|
||||
req := newHttpRequest("/foo")
|
||||
req := newDefaultHttpRequest("/foo")
|
||||
res, _ := doHttpRequest(req, nil)
|
||||
assert.Equal(307, res.StatusCode, "vanilla request should be redirected")
|
||||
|
||||
@ -42,7 +42,7 @@ func TestServerAuthHandler(t *testing.T) {
|
||||
assert.Equal("/o/oauth2/auth", fwd.Path, "vanilla request should be redirected to google")
|
||||
|
||||
// Should catch invalid cookie
|
||||
req = newHttpRequest("/foo")
|
||||
req = newDefaultHttpRequest("/foo")
|
||||
c := MakeCookie(req, "test@example.com")
|
||||
parts := strings.Split(c.Value, "|")
|
||||
c.Value = fmt.Sprintf("bad|%s|%s", parts[1], parts[2])
|
||||
@ -51,7 +51,7 @@ func TestServerAuthHandler(t *testing.T) {
|
||||
assert.Equal(401, res.StatusCode, "invalid cookie should not be authorised")
|
||||
|
||||
// Should validate email
|
||||
req = newHttpRequest("/foo")
|
||||
req = newDefaultHttpRequest("/foo")
|
||||
c = MakeCookie(req, "test@example.com")
|
||||
config.Domains = []string{"test.com"}
|
||||
|
||||
@ -59,7 +59,7 @@ func TestServerAuthHandler(t *testing.T) {
|
||||
assert.Equal(401, res.StatusCode, "invalid email should not be authorised")
|
||||
|
||||
// Should allow valid request email
|
||||
req = newHttpRequest("/foo")
|
||||
req = newDefaultHttpRequest("/foo")
|
||||
c = MakeCookie(req, "test@example.com")
|
||||
config.Domains = []string{}
|
||||
|
||||
@ -91,18 +91,18 @@ func TestServerAuthCallback(t *testing.T) {
|
||||
config.Providers.Google.UserURL = userUrl
|
||||
|
||||
// Should pass auth response request to callback
|
||||
req := newHttpRequest("/_oauth")
|
||||
req := newDefaultHttpRequest("/_oauth")
|
||||
res, _ := doHttpRequest(req, nil)
|
||||
assert.Equal(401, res.StatusCode, "auth callback without cookie shouldn't be authorised")
|
||||
|
||||
// Should catch invalid csrf cookie
|
||||
req = newHttpRequest("/_oauth?state=12345678901234567890123456789012:http://redirect")
|
||||
req = newDefaultHttpRequest("/_oauth?state=12345678901234567890123456789012:http://redirect")
|
||||
c := MakeCSRFCookie(req, "nononononononononononononononono")
|
||||
res, _ = doHttpRequest(req, c)
|
||||
assert.Equal(401, res.StatusCode, "auth callback with invalid cookie shouldn't be authorised")
|
||||
|
||||
// Should redirect valid request
|
||||
req = newHttpRequest("/_oauth?state=12345678901234567890123456789012:http://redirect")
|
||||
req = newDefaultHttpRequest("/_oauth?state=12345678901234567890123456789012:http://redirect")
|
||||
c = MakeCSRFCookie(req, "12345678901234567890123456789012")
|
||||
res, _ = doHttpRequest(req, c)
|
||||
assert.Equal(307, res.StatusCode, "valid auth callback should be allowed")
|
||||
@ -117,33 +117,151 @@ func TestServerDefaultAction(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
config, _ = NewConfig([]string{})
|
||||
|
||||
req := newHttpRequest("/random")
|
||||
req := newDefaultHttpRequest("/random")
|
||||
res, _ := doHttpRequest(req, nil)
|
||||
assert.Equal(307, res.StatusCode, "request should require auth with auth default handler")
|
||||
|
||||
config.DefaultAction = "allow"
|
||||
req = newHttpRequest("/random")
|
||||
req = newDefaultHttpRequest("/random")
|
||||
res, _ = doHttpRequest(req, nil)
|
||||
assert.Equal(200, res.StatusCode, "request should be allowed with default handler")
|
||||
}
|
||||
|
||||
func TestServerRoutePathPrefix(t *testing.T) {
|
||||
func TestServerRouteHeaders(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
config, _ = NewConfig([]string{})
|
||||
config.Rules = map[string]*Rule{
|
||||
"web1": {
|
||||
"1": {
|
||||
Action: "allow",
|
||||
Rule: "PathPrefix(`/api`)",
|
||||
Rule: "Headers(`X-Test`, `test123`)",
|
||||
},
|
||||
"2": {
|
||||
Action: "allow",
|
||||
Rule: "HeadersRegexp(`X-Test`, `test(456|789)`)",
|
||||
},
|
||||
}
|
||||
|
||||
// Should block any request
|
||||
req := newHttpRequest("/random")
|
||||
req := newDefaultHttpRequest("/random")
|
||||
req.Header.Add("X-Random", "hello")
|
||||
res, _ := doHttpRequest(req, nil)
|
||||
assert.Equal(307, res.StatusCode, "request not matching any rule should require auth")
|
||||
|
||||
// Should allow matching
|
||||
req = newDefaultHttpRequest("/api")
|
||||
req.Header.Add("X-Test", "test123")
|
||||
res, _ = doHttpRequest(req, nil)
|
||||
assert.Equal(200, res.StatusCode, "request matching allow rule should be allowed")
|
||||
|
||||
// Should allow matching
|
||||
req = newDefaultHttpRequest("/api")
|
||||
req.Header.Add("X-Test", "test789")
|
||||
res, _ = doHttpRequest(req, nil)
|
||||
assert.Equal(200, res.StatusCode, "request matching allow rule should be allowed")
|
||||
}
|
||||
|
||||
func TestServerRouteHost(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
config, _ = NewConfig([]string{})
|
||||
config.Rules = map[string]*Rule{
|
||||
"1": {
|
||||
Action: "allow",
|
||||
Rule: "Host(`api.example.com`)",
|
||||
},
|
||||
"2": {
|
||||
Action: "allow",
|
||||
Rule: "HostRegexp(`sub{num:[0-9]}.example.com`)",
|
||||
},
|
||||
}
|
||||
|
||||
// Should block any request
|
||||
req := newHttpRequest("GET", "https://example.com/", "/")
|
||||
res, _ := doHttpRequest(req, nil)
|
||||
assert.Equal(307, res.StatusCode, "request not matching any rule should require auth")
|
||||
|
||||
// Should allow matching request
|
||||
req = newHttpRequest("GET", "https://api.example.com/", "/")
|
||||
res, _ = doHttpRequest(req, nil)
|
||||
assert.Equal(200, res.StatusCode, "request matching allow rule should be allowed")
|
||||
|
||||
// Should allow matching request
|
||||
req = newHttpRequest("GET", "https://sub8.example.com/", "/")
|
||||
res, _ = doHttpRequest(req, nil)
|
||||
assert.Equal(200, res.StatusCode, "request matching allow rule should be allowed")
|
||||
}
|
||||
|
||||
func TestServerRouteMethod(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
config, _ = NewConfig([]string{})
|
||||
config.Rules = map[string]*Rule{
|
||||
"1": {
|
||||
Action: "allow",
|
||||
Rule: "Method(`PUT`)",
|
||||
},
|
||||
}
|
||||
|
||||
// Should block any request
|
||||
req := newHttpRequest("GET", "https://example.com/", "/")
|
||||
res, _ := doHttpRequest(req, nil)
|
||||
assert.Equal(307, res.StatusCode, "request not matching any rule should require auth")
|
||||
|
||||
// Should allow matching request
|
||||
req = newHttpRequest("PUT", "https://example.com/", "/")
|
||||
res, _ = doHttpRequest(req, nil)
|
||||
assert.Equal(200, res.StatusCode, "request matching allow rule should be allowed")
|
||||
}
|
||||
|
||||
func TestServerRoutePath(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
config, _ = NewConfig([]string{})
|
||||
config.Rules = map[string]*Rule{
|
||||
"1": {
|
||||
Action: "allow",
|
||||
Rule: "Path(`/api`)",
|
||||
},
|
||||
"2": {
|
||||
Action: "allow",
|
||||
Rule: "PathPrefix(`/private`)",
|
||||
},
|
||||
}
|
||||
|
||||
// Should block any request
|
||||
req := newDefaultHttpRequest("/random")
|
||||
res, _ := doHttpRequest(req, nil)
|
||||
assert.Equal(307, res.StatusCode, "request not matching any rule should require auth")
|
||||
|
||||
// Should allow /api request
|
||||
req = newHttpRequest("/api")
|
||||
req = newDefaultHttpRequest("/api")
|
||||
res, _ = doHttpRequest(req, nil)
|
||||
assert.Equal(200, res.StatusCode, "request matching allow rule should be allowed")
|
||||
|
||||
// Should allow /private request
|
||||
req = newDefaultHttpRequest("/private")
|
||||
res, _ = doHttpRequest(req, nil)
|
||||
assert.Equal(200, res.StatusCode, "request matching allow rule should be allowed")
|
||||
|
||||
req = newDefaultHttpRequest("/private/path")
|
||||
res, _ = doHttpRequest(req, nil)
|
||||
assert.Equal(200, res.StatusCode, "request matching allow rule should be allowed")
|
||||
}
|
||||
|
||||
func TestServerRouteQuery(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
config, _ = NewConfig([]string{})
|
||||
config.Rules = map[string]*Rule{
|
||||
"1": {
|
||||
Action: "allow",
|
||||
Rule: "Query(`q=test123`)",
|
||||
},
|
||||
}
|
||||
|
||||
// Should block any request
|
||||
req := newHttpRequest("GET", "https://example.com/", "/?q=no")
|
||||
res, _ := doHttpRequest(req, nil)
|
||||
assert.Equal(307, res.StatusCode, "request not matching any rule should require auth")
|
||||
|
||||
// Should allow matching request
|
||||
req = newHttpRequest("GET", "https://api.example.com/", "/?q=test123")
|
||||
res, _ = doHttpRequest(req, nil)
|
||||
assert.Equal(200, res.StatusCode, "request matching allow rule should be allowed")
|
||||
}
|
||||
@ -194,8 +312,15 @@ func doHttpRequest(r *http.Request, c *http.Cookie) (*http.Response, string) {
|
||||
return res, string(body)
|
||||
}
|
||||
|
||||
func newHttpRequest(uri string) *http.Request {
|
||||
r := httptest.NewRequest("", "http://example.com/", nil)
|
||||
func newDefaultHttpRequest(uri string) *http.Request {
|
||||
return newHttpRequest("", "http://example.com/", uri)
|
||||
}
|
||||
|
||||
func newHttpRequest(method, dest, uri string) *http.Request {
|
||||
r := httptest.NewRequest("", "http://should-use-x-forwarded.com", nil)
|
||||
p, _ := url.Parse(dest)
|
||||
r.Header.Add("X-Forwarded-Method", method)
|
||||
r.Header.Add("X-Forwarded-Host", p.Host)
|
||||
r.Header.Add("X-Forwarded-Uri", uri)
|
||||
return r
|
||||
}
|
||||
|
@ -1,3 +1,5 @@
|
||||
cookie-name=inicookiename
|
||||
csrf-cookie-name=inicsrfcookiename
|
||||
url-path=one
|
||||
rule.1.action=allow
|
||||
rule.1.rule=PathPrefix(`/one`)
|
||||
|
@ -1 +1,3 @@
|
||||
url-path=two
|
||||
rule.two.action=auth
|
||||
rule.two.rule=Host(`two.com`) && Path(`/two`)
|
||||
|
Reference in New Issue
Block a user