9 Commits

Author SHA1 Message Date
d890a4aad6 Add more v2 tests + fixes + improve legacy config parsing 2019-04-18 15:27:41 +01:00
43775591fa Use new rule config + tidy ups 2019-04-17 11:29:35 +01:00
daec9f591a Improve qsdiff error reporting 2019-04-15 12:03:31 +01:00
091590d391 another attempt at dev travis 2019-04-12 16:47:36 +01:00
8ca16a88d2 fix dev travis 2019-04-12 16:34:23 +01:00
814892a88b Refactor progress
- move directory structure
- string based rule definition
- use traefik rule parsing
- drop toml config
- new flag library
- implement go dep
2019-04-12 16:12:13 +01:00
19c249a6d1 fix dockerfile 2019-01-30 18:06:46 +00:00
0f278d516b fix tests 2019-01-30 17:11:41 +00:00
ae95e8b2e5 Begin refactor + selective auth 2019-01-30 16:52:47 +00:00
15 changed files with 534 additions and 634 deletions

310
README.md
View File

@ -1,306 +1,90 @@
# Traefik Forward Auth [![Build Status](https://travis-ci.org/thomseddon/traefik-forward-auth.svg?branch=master)](https://travis-ci.org/thomseddon/traefik-forward-auth) [![Go Report Card](https://goreportcard.com/badge/github.com/thomseddon/traefik-forward-auth)](https://goreportcard.com/report/github.com/thomseddon/traefik-forward-auth) ![Docker Pulls](https://img.shields.io/docker/pulls/thomseddon/traefik-forward-auth.svg) [![GitHub release](https://img.shields.io/github/release/thomseddon/traefik-forward-auth.svg)](https://GitHub.com/thomseddon/traefik-forward-auth/releases/) # Traefik Forward Auth [![Build Status](https://travis-ci.org/thomseddon/traefik-forward-auth.svg?branch=master)](https://travis-ci.org/thomseddon/traefik-forward-auth) [![Go Report Card](https://goreportcard.com/badge/github.com/thomseddon/traefik-forward-auth)](https://goreportcard.com/badge/github.com/thomseddon/traefik-forward-auth)
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? ## 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 - 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)) - 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))
# Contents ## Quick Start
- [Usage](#installation) 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.
- [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)
## Usage
#### Simple:
See below for instructions on how to setup your [OAuth Configuration](#oauth-configuration).
docker-compose.yml:
```yaml
version: '3'
services:
traefik:
image: traefik:1
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 compatibile, 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 ## Configuration
### Overview The following configuration is supported:
The following configuration options are supported:
``` |Flag |Type |Description|
Usage: |-----------------------|------|-----------|
traefik-forward-auth [OPTIONS] |-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")|
Application Options: Configuration can also be supplied as environment variables (use upper case and swap `-`'s for `_`'s e.g. `-client-id` becomes `CLIENT_ID`)
--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: 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`)
--providers.google.client-id= Client ID [$CLIENT_ID]
--providers.google.client-secret= Client Secret [$CLIENT_SECRET]
--providers.google.prompt= Space separated list of OpenID prompt options [$PROMPT]
Help Options: ## OAuth Configuration
-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): Head to https://console.developers.google.com & make sure you've switched to the correct email account.
1. **Command Arguments/Flags** - As shown above Create a new project then search for and select "Credentials" in the search bar. Fill out the "OAuth Consent Screen" tab.
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 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)
- `auth-host` ## Usage
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 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).
The host should be specified without protocol or path, for example: ## User Restriction
```
--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: 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 `whitelist` then only this is checked and `domain` is effectively ignored.
### Forwarded Headers ## Cookie Domains
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). 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.
### Operation Modes 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.
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.
## Operation Modes
#### Overlay #### Overlay
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 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.
The user flow will be: 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`.
1. Request to `www.myapp.com/home` 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)
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
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)
#### Auth Host #### 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/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/example/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. 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`.

View File

@ -25,10 +25,10 @@ services:
traefik-forward-auth: traefik-forward-auth:
image: thomseddon/traefik-forward-auth image: thomseddon/traefik-forward-auth
environment: environment:
- PROVIDERS_GOOGLE_CLIENT_ID=your-client-id - CLIENT_ID=your-client-id
- PROVIDERS_GOOGLE_CLIENT_SECRET=your-client-secret - CLIENT_SECRET=your-client-secret
- SECRET=something-random - SECRET=something-random
- INSECURE_COOKIE=true - COOKIE_SECURE=false
- DOMAIN=yourcompany.com - DOMAIN=yourcompany.com
- AUTH_HOST=auth.yourdomain.com - AUTH_HOST=auth.yourdomain.com
networks: networks:

View File

@ -35,12 +35,12 @@ services:
traefik-forward-auth: traefik-forward-auth:
build: ../ build: ../
environment: environment:
- PROVIDERS_GOOGLE_CLIENT_ID=your-client-id - CLIENT_ID=test
- PROVIDERS_GOOGLE_CLIENT_SECRET=your-client-secret - CLIENT_SECRET=test
- SECRET=something-random - COOKIE_SECRET=something-random
- INSECURE_COOKIE=true - COOKIE_SECURE=false
- COOKIE_DOMAIN=localhost.com - COOKIE_DOMAINS=localhost.com
- AUTH_HOST=auth.localhost.com - AUTH_URL=http://auth.localhost.com:8085/_oauth
networks: networks:
- traefik - traefik

View File

@ -25,10 +25,10 @@ services:
traefik-forward-auth: traefik-forward-auth:
image: thomseddon/traefik-forward-auth image: thomseddon/traefik-forward-auth
environment: environment:
- PROVIDERS_GOOGLE_CLIENT_ID=your-client-id - CLIENT_ID=your-client-id
- PROVIDERS_GOOGLE_CLIENT_SECRET=your-client-secret - CLIENT_SECRET=your-client-secret
- SECRET=something-random - SECRET=something-random
- INSECURE_COOKIE=true - COOKIE_SECURE=false
- DOMAIN=yourcompany.com - DOMAIN=yourcompany.com
networks: networks:
- traefik - traefik

14
go.mod
View File

@ -9,27 +9,23 @@ require (
github.com/containous/flaeg v1.4.1 // indirect github.com/containous/flaeg v1.4.1 // indirect
github.com/containous/mux v0.0.0-20181024131434-c33f32e26898 // indirect github.com/containous/mux v0.0.0-20181024131434-c33f32e26898 // indirect
github.com/containous/traefik v2.0.0-alpha2+incompatible github.com/containous/traefik v2.0.0-alpha2+incompatible
github.com/go-acme/lego v2.5.0+incompatible // indirect github.com/go-acme/lego v2.4.0+incompatible // indirect
github.com/go-kit/kit v0.8.0 // indirect github.com/go-kit/kit v0.8.0 // indirect
github.com/gorilla/context v1.1.1 // indirect github.com/gorilla/context v1.1.1 // indirect
github.com/gravitational/trace v0.0.0-20190409171327-f30095ced5ff // 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.0
github.com/jonboulle/clockwork v0.1.0 // indirect 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 github.com/kr/pretty v0.1.0 // indirect
github.com/kr/pty v1.1.4 // indirect
github.com/miekg/dns v1.1.8 // indirect github.com/miekg/dns v1.1.8 // indirect
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
github.com/pkg/errors v0.8.1 // indirect github.com/pkg/errors v0.8.1 // indirect
github.com/ryanuber/go-glob v1.0.0 // indirect github.com/ryanuber/go-glob v1.0.0 // indirect
github.com/sirupsen/logrus v1.4.1 github.com/sirupsen/logrus v1.4.1
github.com/stretchr/objx v0.2.0 // indirect github.com/stretchr/testify v1.3.0 // indirect
github.com/stretchr/testify v1.3.0
github.com/vulcand/predicate v1.1.0 // indirect github.com/vulcand/predicate v1.1.0 // indirect
golang.org/x/crypto v0.0.0-20190422183909-d864b10871cd // indirect golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a // indirect
golang.org/x/net v0.0.0-20190420063019-afa5a82059c6 // indirect golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 // indirect
golang.org/x/sync v0.0.0-20190423024810-112230192c58 // indirect golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f // indirect
golang.org/x/sys v0.0.0-20190422165155-953cdadca894 // indirect
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
gopkg.in/square/go-jose.v2 v2.3.1 // indirect gopkg.in/square/go-jose.v2 v2.3.1 // indirect
) )

12
go.sum
View File

@ -15,7 +15,6 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-acme/lego v2.4.0+incompatible h1:+BTLUfLtDc5qQauyiTCXH6lupEUOCvXyGlEjdeU0YQI= github.com/go-acme/lego v2.4.0+incompatible h1:+BTLUfLtDc5qQauyiTCXH6lupEUOCvXyGlEjdeU0YQI=
github.com/go-acme/lego v2.4.0+incompatible/go.mod h1:yzMNe9CasVUhkquNvti5nAtPmG94USbYxYrZfTkIn0M= github.com/go-acme/lego v2.4.0+incompatible/go.mod h1:yzMNe9CasVUhkquNvti5nAtPmG94USbYxYrZfTkIn0M=
github.com/go-acme/lego v2.5.0+incompatible/go.mod h1:yzMNe9CasVUhkquNvti5nAtPmG94USbYxYrZfTkIn0M=
github.com/go-kit/kit v0.8.0 h1:Wz+5lgoB0kkuqLEc6NVmwRknTKP6dTGbSqvhZtBI/j0= 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/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8= github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
@ -28,11 +27,9 @@ github.com/jonboulle/clockwork v0.1.0 h1:VKV+ZcuP6l3yW9doeqz6ziZGgcynBVQO+obU0+0
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= 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/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.4/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/miekg/dns v1.1.8 h1:1QYRAKU3lN5cRfLCkPU08hwvLJFhvjP6MqNMmQz6ZVI= github.com/miekg/dns v1.1.8 h1:1QYRAKU3lN5cRfLCkPU08hwvLJFhvjP6MqNMmQz6ZVI=
@ -49,7 +46,6 @@ 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/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/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
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 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=
@ -58,23 +54,15 @@ github.com/vulcand/predicate v1.1.0/go.mod h1:mlccC5IRBoc2cIFmCB8ZM62I3VDb6p2GXE
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a h1:Igim7XhdOpBnWPuYJ70XcNpq8q3BCACtVgNfoJxOV7g= golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a h1:Igim7XhdOpBnWPuYJ70XcNpq8q3BCACtVgNfoJxOV7g=
golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
golang.org/x/crypto v0.0.0-20190422183909-d864b10871cd h1:sMHc2rZHuzQmrbVoSpt9HgerkXPyIeCSO6k0zUMGfFk=
golang.org/x/crypto v0.0.0-20190422183909-d864b10871cd/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ= 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-20190420063019-afa5a82059c6 h1:HdqqaWmYAUI7/dmByKKEw+yxDksGSo+9GjkUc9Zp34E=
golang.org/x/net v0.0.0-20190420063019-afa5a82059c6/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33 h1:I6FyU15t786LL7oL/hn43zqTuEGr4PN7F4XJ1p4E3Y8= 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-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e h1:nFYrTHrdrAOpShe27kaFHjsqYSEQ0KWqdWLu3xuZJts= golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e h1:nFYrTHrdrAOpShe27kaFHjsqYSEQ0KWqdWLu3xuZJts=
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-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View File

@ -311,20 +311,10 @@ func (c *CookieDomain) Match(host string) bool {
return false return false
} }
func (c *CookieDomain) UnmarshalFlag(value string) error {
*c = *NewCookieDomain(value)
return nil
}
func (c *CookieDomain) MarshalFlag() (string, error) {
return c.Domain, nil
}
// Legacy support for comma separated list of cookie domains
type CookieDomains []CookieDomain type CookieDomains []CookieDomain
func (c *CookieDomains) UnmarshalFlag(value string) error { func (c *CookieDomains) UnmarshalFlag(value string) error {
// TODO: test
if len(value) > 0 { if len(value) > 0 {
for _, d := range strings.Split(value, ",") { for _, d := range strings.Split(value, ",") {
cookieDomain := NewCookieDomain(d) cookieDomain := NewCookieDomain(d)

View File

@ -4,11 +4,11 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"net/url" "net/url"
"reflect"
"strings" "strings"
"testing" "testing"
"time" "time"
"github.com/stretchr/testify/assert"
"github.com/thomseddon/traefik-forward-auth/internal/provider" "github.com/thomseddon/traefik-forward-auth/internal/provider"
) )
@ -17,7 +17,6 @@ import (
*/ */
func TestAuthValidateCookie(t *testing.T) { func TestAuthValidateCookie(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 := &http.Cookie{} c := &http.Cookie{}
@ -25,85 +24,87 @@ func TestAuthValidateCookie(t *testing.T) {
// Should require 3 parts // Should require 3 parts
c.Value = "" c.Value = ""
valid, _, err := ValidateCookie(r, c) valid, _, err := ValidateCookie(r, c)
assert.False(valid) if valid || err.Error() != "Invalid cookie format" {
if assert.Error(err) { t.Error("Should get \"Invalid cookie format\", got:", err)
assert.Equal("Invalid cookie format", err.Error())
} }
c.Value = "1|2" c.Value = "1|2"
valid, _, err = ValidateCookie(r, c) valid, _, err = ValidateCookie(r, c)
assert.False(valid) if valid || err.Error() != "Invalid cookie format" {
if assert.Error(err) { t.Error("Should get \"Invalid cookie format\", got:", err)
assert.Equal("Invalid cookie format", err.Error())
} }
c.Value = "1|2|3|4" c.Value = "1|2|3|4"
valid, _, err = ValidateCookie(r, c) valid, _, err = ValidateCookie(r, c)
assert.False(valid) if valid || err.Error() != "Invalid cookie format" {
if assert.Error(err) { t.Error("Should get \"Invalid cookie format\", got:", err)
assert.Equal("Invalid cookie format", err.Error())
} }
// Should catch invalid mac // Should catch invalid mac
c.Value = "MQ==|2|3" c.Value = "MQ==|2|3"
valid, _, err = ValidateCookie(r, c) valid, _, err = ValidateCookie(r, c)
assert.False(valid) if valid || err.Error() != "Invalid cookie mac" {
if assert.Error(err) { t.Error("Should get \"Invalid cookie mac\", got:", err)
assert.Equal("Invalid cookie mac", err.Error())
} }
// Should catch expired // Should catch expired
config.Lifetime = time.Second * time.Duration(-1) config.Lifetime = time.Second * time.Duration(-1)
c = MakeCookie(r, "test@test.com") c = MakeCookie(r, "test@test.com")
valid, _, err = ValidateCookie(r, c) valid, _, err = ValidateCookie(r, c)
assert.False(valid) if valid || err.Error() != "Cookie has expired" {
if assert.Error(err) { t.Error("Should get \"Cookie has expired\", got:", err)
assert.Equal("Cookie has expired", err.Error())
} }
// Should accept valid cookie // Should accept valid cookie
config.Lifetime = time.Second * time.Duration(10) config.Lifetime = time.Second * time.Duration(10)
c = MakeCookie(r, "test@test.com") c = MakeCookie(r, "test@test.com")
valid, email, err := ValidateCookie(r, c) valid, email, err := ValidateCookie(r, c)
assert.True(valid, "valid request should return valid") if !valid {
assert.Nil(err, "valid request should not return an error") t.Error("Valid request should return as valid")
assert.Equal("test@test.com", email, "valid request should return user email") }
if err != nil {
t.Error("Valid request should not return error, got:", err)
}
if email != "test@test.com" {
t.Error("Valid request should return user email")
}
} }
func TestAuthValidateEmail(t *testing.T) { func TestAuthValidateEmail(t *testing.T) {
assert := assert.New(t)
config, _ = NewConfig([]string{}) config, _ = NewConfig([]string{})
// Should allow any // Should allow any
v := ValidateEmail("test@test.com") if !ValidateEmail("test@test.com") || !ValidateEmail("one@two.com") {
assert.True(v, "should allow any domain if email domain is not defined") t.Error("Should allow any domain if email domain is not defined")
v = ValidateEmail("one@two.com") }
assert.True(v, "should allow any domain if email domain is not defined")
// Should block non matching domain // Should block non matching domain
config.Domains = []string{"test.com"} config.Domains = []string{"test.com"}
v = ValidateEmail("one@two.com") if ValidateEmail("one@two.com") {
assert.False(v, "should not allow user from another domain") t.Error("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") if !ValidateEmail("test@test.com") {
assert.True(v, "should allow user from allowed domain") t.Error("Should allow user from allowed domain")
}
// Should block non whitelisted email address // Should block non whitelisted email address
config.Domains = []string{} config.Domains = []string{}
config.Whitelist = []string{"test@test.com"} config.Whitelist = []string{"test@test.com"}
v = ValidateEmail("one@two.com") if ValidateEmail("one@two.com") {
assert.False(v, "should not allow user not in whitelist") t.Error("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") if !ValidateEmail("test@test.com") {
assert.True(v, "should allow user in whitelist") t.Error("Should allow user in whitelist.")
}
} }
// TODO: Split google tests out // TODO: Split google tests out
func TestAuthGetLoginURL(t *testing.T) { func TestAuthGetLoginURL(t *testing.T) {
assert := assert.New(t)
google := provider.Google{ google := provider.Google{
ClientId: "idtest", ClientId: "idtest",
ClientSecret: "sectest", ClientSecret: "sectest",
@ -126,10 +127,18 @@ func TestAuthGetLoginURL(t *testing.T) {
// Check url // Check url
uri, err := url.Parse(GetLoginURL(r, "nonce")) uri, err := url.Parse(GetLoginURL(r, "nonce"))
assert.Nil(err) if err != nil {
assert.Equal("https", uri.Scheme) t.Error("Error parsing login url:", err)
assert.Equal("test.com", uri.Host) }
assert.Equal("/auth", uri.Path) if uri.Scheme != "https" {
t.Error("Expected login Scheme to be \"https\", got:", uri.Scheme)
}
if uri.Host != "test.com" {
t.Error("Expected login Host to be \"test.com\", got:", uri.Host)
}
if uri.Path != "/auth" {
t.Error("Expected login Path to be \"/auth\", got:", uri.Path)
}
// Check query string // Check query string
qs := uri.Query() qs := uri.Query()
@ -141,7 +150,11 @@ func TestAuthGetLoginURL(t *testing.T) {
"prompt": []string{"consent select_account"}, "prompt": []string{"consent select_account"},
"state": []string{"nonce:http://example.com/hello"}, "state": []string{"nonce:http://example.com/hello"},
} }
assert.Equal(expectedQs, qs) if !reflect.DeepEqual(qs, expectedQs) {
for _, err := range qsDiff(t, expectedQs, qs) {
t.Error(err)
}
}
// //
// With Auth URL but no matching cookie domain // With Auth URL but no matching cookie domain
@ -153,10 +166,18 @@ func TestAuthGetLoginURL(t *testing.T) {
// Check url // Check url
uri, err = url.Parse(GetLoginURL(r, "nonce")) uri, err = url.Parse(GetLoginURL(r, "nonce"))
assert.Nil(err) if err != nil {
assert.Equal("https", uri.Scheme) t.Error("Error parsing login url:", err)
assert.Equal("test.com", uri.Host) }
assert.Equal("/auth", uri.Path) if uri.Scheme != "https" {
t.Error("Expected login Scheme to be \"https\", got:", uri.Scheme)
}
if uri.Host != "test.com" {
t.Error("Expected login Host to be \"test.com\", got:", uri.Host)
}
if uri.Path != "/auth" {
t.Error("Expected login Path to be \"/auth\", got:", uri.Path)
}
// Check query string // Check query string
qs = uri.Query() qs = uri.Query()
@ -168,7 +189,11 @@ func TestAuthGetLoginURL(t *testing.T) {
"prompt": []string{"consent select_account"}, "prompt": []string{"consent select_account"},
"state": []string{"nonce:http://example.com/hello"}, "state": []string{"nonce:http://example.com/hello"},
} }
assert.Equal(expectedQs, qs) if !reflect.DeepEqual(qs, expectedQs) {
for _, err := range qsDiff(t, expectedQs, qs) {
t.Error(err)
}
}
// //
// With correct Auth URL + cookie domain // With correct Auth URL + cookie domain
@ -180,10 +205,18 @@ func TestAuthGetLoginURL(t *testing.T) {
// Check url // Check url
uri, err = url.Parse(GetLoginURL(r, "nonce")) uri, err = url.Parse(GetLoginURL(r, "nonce"))
assert.Nil(err) if err != nil {
assert.Equal("https", uri.Scheme) t.Error("Error parsing login url:", err)
assert.Equal("test.com", uri.Host) }
assert.Equal("/auth", uri.Path) if uri.Scheme != "https" {
t.Error("Expected login Scheme to be \"https\", got:", uri.Scheme)
}
if uri.Host != "test.com" {
t.Error("Expected login Host to be \"test.com\", got:", uri.Host)
}
if uri.Path != "/auth" {
t.Error("Expected login Path to be \"/auth\", got:", uri.Path)
}
// Check query string // Check query string
qs = uri.Query() qs = uri.Query()
@ -195,7 +228,14 @@ func TestAuthGetLoginURL(t *testing.T) {
"state": []string{"nonce:http://example.com/hello"}, "state": []string{"nonce:http://example.com/hello"},
"prompt": []string{"consent select_account"}, "prompt": []string{"consent select_account"},
} }
assert.Equal(expectedQs, qs) for _, err := range qsDiff(t, expectedQs, qs) {
t.Error(err)
}
if !reflect.DeepEqual(qs, expectedQs) {
for _, err := range qsDiff(t, expectedQs, qs) {
t.Error(err)
}
}
// //
// With Auth URL + cookie domain, but from different domain // With Auth URL + cookie domain, but from different domain
@ -208,10 +248,18 @@ func TestAuthGetLoginURL(t *testing.T) {
// Check url // Check url
uri, err = url.Parse(GetLoginURL(r, "nonce")) uri, err = url.Parse(GetLoginURL(r, "nonce"))
assert.Nil(err) if err != nil {
assert.Equal("https", uri.Scheme) t.Error("Error parsing login url:", err)
assert.Equal("test.com", uri.Host) }
assert.Equal("/auth", uri.Path) if uri.Scheme != "https" {
t.Error("Expected login Scheme to be \"https\", got:", uri.Scheme)
}
if uri.Host != "test.com" {
t.Error("Expected login Host to be \"test.com\", got:", uri.Host)
}
if uri.Path != "/auth" {
t.Error("Expected login Path to be \"/auth\", got:", uri.Path)
}
// Check query string // Check query string
qs = uri.Query() qs = uri.Query()
@ -223,7 +271,14 @@ func TestAuthGetLoginURL(t *testing.T) {
"state": []string{"nonce:http://another.com/hello"}, "state": []string{"nonce:http://another.com/hello"},
"prompt": []string{"consent select_account"}, "prompt": []string{"consent select_account"},
} }
assert.Equal(expectedQs, qs) for _, err := range qsDiff(t, expectedQs, qs) {
t.Error(err)
}
if !reflect.DeepEqual(qs, expectedQs) {
for _, err := range qsDiff(t, expectedQs, qs) {
t.Error(err)
}
}
} }
// TODO // TODO
@ -235,47 +290,68 @@ func TestAuthGetLoginURL(t *testing.T) {
// } // }
func TestAuthMakeCookie(t *testing.T) { func TestAuthMakeCookie(t *testing.T) {
assert := assert.New(t)
config, _ = NewConfig([]string{}) config, _ = NewConfig([]string{})
r, _ := http.NewRequest("GET", "http://app.example.com", nil) r, _ := http.NewRequest("GET", "http://app.example.com", nil)
r.Header.Add("X-Forwarded-Host", "app.example.com") r.Header.Add("X-Forwarded-Host", "app.example.com")
c := MakeCookie(r, "test@example.com") c := MakeCookie(r, "test@example.com")
assert.Equal("_forward_auth", c.Name) if c.Name != "_forward_auth" {
t.Error("Cookie name should be \"_forward_auth\", got:", c.Name)
}
parts := strings.Split(c.Value, "|") parts := strings.Split(c.Value, "|")
assert.Len(parts, 3, "cookie should be 3 parts") if len(parts) != 3 {
t.Error("Cookie should be in 3 parts, got:", c.Value)
}
valid, _, _ := ValidateCookie(r, c) valid, _, _ := ValidateCookie(r, c)
assert.True(valid, "should generate valid cookie") if !valid {
assert.Equal("/", c.Path) t.Error("Should generate valid cookie:", c.Value)
assert.Equal("app.example.com", c.Domain) }
assert.True(c.Secure) if c.Path != "/" {
t.Error("Cookie path should be \"/\", got:", c.Path)
expires := time.Now().Local().Add(config.Lifetime) }
assert.WithinDuration(expires, c.Expires, 10*time.Second) if c.Domain != "app.example.com" {
t.Error("Cookie domain should be \"app.example.com\", got:", c.Domain)
}
if c.Secure != true {
t.Error("Cookie domain should be true, got:", c.Secure)
}
if !c.Expires.After(time.Now().Local()) {
t.Error("Expires should be after now, got:", c.Expires)
}
if !c.Expires.Before(time.Now().Local().Add(config.Lifetime).Add(10 * time.Second)) {
t.Error("Expires should be before lifetime + 10 seconds, got:", c.Expires)
}
config.CookieName = "testname" config.CookieName = "testname"
config.InsecureCookie = true config.InsecureCookie = true
c = MakeCookie(r, "test@example.com") c = MakeCookie(r, "test@example.com")
assert.Equal("testname", c.Name) if c.Name != "testname" {
assert.False(c.Secure) t.Error("Cookie name should be \"testname\", got:", c.Name)
}
if c.Secure != false {
t.Error("Cookie domain should be false, got:", c.Secure)
}
} }
func TestAuthMakeCSRFCookie(t *testing.T) { func TestAuthMakeCSRFCookie(t *testing.T) {
assert := assert.New(t)
config, _ = NewConfig([]string{}) config, _ = NewConfig([]string{})
r, _ := http.NewRequest("GET", "http://app.example.com", nil) r, _ := http.NewRequest("GET", "http://app.example.com", nil)
r.Header.Add("X-Forwarded-Host", "app.example.com") r.Header.Add("X-Forwarded-Host", "app.example.com")
// No cookie domain or auth url // No cookie domain or auth url
c := MakeCSRFCookie(r, "12345678901234567890123456789012") c := MakeCSRFCookie(r, "12345678901234567890123456789012")
assert.Equal("app.example.com", c.Domain) if c.Domain != "app.example.com" {
t.Error("Cookie Domain should match request domain, got:", c.Domain)
}
// With cookie domain but no auth url // With cookie domain but no auth url
config = Config{ config = Config{
CookieDomains: []CookieDomain{*NewCookieDomain("example.com")}, CookieDomains: []CookieDomain{*NewCookieDomain("example.com")},
} }
c = MakeCSRFCookie(r, "12345678901234567890123456789012") c = MakeCSRFCookie(r, "12345678901234567890123456789012")
assert.Equal("app.example.com", c.Domain) if c.Domain != "app.example.com" {
t.Error("Cookie Domain should match request domain, got:", c.Domain)
}
// With cookie domain and auth url // With cookie domain and auth url
config = Config{ config = Config{
@ -283,7 +359,9 @@ func TestAuthMakeCSRFCookie(t *testing.T) {
CookieDomains: []CookieDomain{*NewCookieDomain("example.com")}, CookieDomains: []CookieDomain{*NewCookieDomain("example.com")},
} }
c = MakeCSRFCookie(r, "12345678901234567890123456789012") c = MakeCSRFCookie(r, "12345678901234567890123456789012")
assert.Equal("example.com", c.Domain) if c.Domain != "example.com" {
t.Error("Cookie Domain should match request domain, got:", c.Domain)
}
} }
func TestAuthClearCSRFCookie(t *testing.T) { func TestAuthClearCSRFCookie(t *testing.T) {
@ -297,7 +375,6 @@ func TestAuthClearCSRFCookie(t *testing.T) {
} }
func TestAuthValidateCSRFCookie(t *testing.T) { func TestAuthValidateCSRFCookie(t *testing.T) {
assert := assert.New(t)
config, _ = NewConfig([]string{}) config, _ = NewConfig([]string{})
c := &http.Cookie{} c := &http.Cookie{}
@ -311,88 +388,103 @@ func TestAuthValidateCSRFCookie(t *testing.T) {
r := newCsrfRequest("") r := newCsrfRequest("")
c.Value = "" c.Value = ""
valid, _, err := ValidateCSRFCookie(r, c) valid, _, err := ValidateCSRFCookie(r, c)
assert.False(valid) if valid || err.Error() != "Invalid CSRF cookie value" {
if assert.Error(err) { t.Error("Should get \"Invalid CSRF cookie value\", got:", err)
assert.Equal("Invalid CSRF cookie value", err.Error())
} }
c.Value = "123456789012345678901234567890123" c.Value = "123456789012345678901234567890123"
valid, _, err = ValidateCSRFCookie(r, c) valid, _, err = ValidateCSRFCookie(r, c)
assert.False(valid) if valid || err.Error() != "Invalid CSRF cookie value" {
if assert.Error(err) { t.Error("Should get \"Invalid CSRF cookie value\", got:", err)
assert.Equal("Invalid CSRF cookie value", err.Error())
} }
// Should require valid state // Should require valid state
r = newCsrfRequest("12345678901234567890123456789012:") r = newCsrfRequest("12345678901234567890123456789012:")
c.Value = "12345678901234567890123456789012" c.Value = "12345678901234567890123456789012"
valid, _, err = ValidateCSRFCookie(r, c) valid, _, err = ValidateCSRFCookie(r, c)
assert.False(valid) if valid || err.Error() != "Invalid CSRF state value" {
if assert.Error(err) { t.Error("Should get \"Invalid CSRF state value\", got:", err)
assert.Equal("Invalid CSRF state value", err.Error())
} }
// Should allow valid state // Should allow valid state
r = newCsrfRequest("12345678901234567890123456789012:99") r = newCsrfRequest("12345678901234567890123456789012:99")
c.Value = "12345678901234567890123456789012" c.Value = "12345678901234567890123456789012"
valid, state, err := ValidateCSRFCookie(r, c) valid, state, err := ValidateCSRFCookie(r, c)
assert.True(valid, "valid request should return valid") if !valid {
assert.Nil(err, "valid request should not return an error") t.Error("Valid request should return as valid")
assert.Equal("99", state, "valid request should return correct state") }
if err != nil {
t.Error("Valid request should not return error, got:", err)
}
if state != "99" {
t.Error("Valid request should return correct state, got:", state)
}
} }
func TestAuthNonce(t *testing.T) { func TestAuthNonce(t *testing.T) {
assert := assert.New(t)
err, nonce1 := Nonce() err, nonce1 := Nonce()
assert.Nil(err, "error generating nonce") if err != nil {
assert.Len(nonce1, 32, "length should be 32 chars") t.Error("Error generation nonce:", err)
}
err, nonce2 := Nonce() err, nonce2 := Nonce()
assert.Nil(err, "error generating nonce") if err != nil {
assert.Len(nonce2, 32, "length should be 32 chars") t.Error("Error generation nonce:", err)
}
assert.NotEqual(nonce1, nonce2, "nonce should not be equal") if len(nonce1) != 32 || len(nonce2) != 32 {
t.Error("Nonce should be 32 chars")
}
if nonce1 == nonce2 {
t.Error("Nonce should not be equal")
}
} }
func TestAuthCookieDomainMatch(t *testing.T) { func TestAuthCookieDomainMatch(t *testing.T) {
assert := assert.New(t)
cd := NewCookieDomain("example.com") cd := NewCookieDomain("example.com")
// Exact should match // Exact should match
assert.True(cd.Match("example.com"), "exact domain should match") if !cd.Match("example.com") {
t.Error("Exact domain should match")
}
// Subdomain should match // Subdomain should match
assert.True(cd.Match("test.example.com"), "subdomain should match") if !cd.Match("test.example.com") {
t.Error("Subdomain should match")
}
// Derived domain should not match // Derived domain should not match
assert.False(cd.Match("testexample.com"), "derived domain should not match") if cd.Match("testexample.com") {
t.Error("Derived domain should not match")
}
// Other domain should not match // Other domain should not match
assert.False(cd.Match("test.com"), "other domain should not match") if cd.Match("test.com") {
t.Error("Other domain should not match")
}
} }
func TestAuthCookieDomains(t *testing.T) { func TestAuthCookieDomains(t *testing.T) {
assert := assert.New(t)
cds := CookieDomains{} cds := CookieDomains{}
err := cds.UnmarshalFlag("one.com,two.org") err := cds.UnmarshalFlag("one.com,two.org")
assert.Nil(err) if err != nil {
expected := CookieDomains{ t.Error(err)
CookieDomain{ }
Domain: "one.com", if len(cds) != 2 {
DomainLen: 7, t.Error("Expected UnmarshalFlag to provide 2 CookieDomains, got", cds)
SubDomain: ".one.com", }
SubDomainLen: 8, if cds[0].Domain != "one.com" || cds[0].SubDomain != ".one.com" {
}, t.Error("Expected UnmarshalFlag to provide one.com, got", cds[0])
CookieDomain{ }
Domain: "two.org", if cds[1].Domain != "two.org" || cds[1].SubDomain != ".two.org" {
DomainLen: 7, t.Error("Expected UnmarshalFlag to provide two.org, got", cds[1])
SubDomain: ".two.org",
SubDomainLen: 8,
},
} }
assert.Equal(expected, cds)
marshal, err := cds.MarshalFlag() marshal, err := cds.MarshalFlag()
assert.Nil(err) if err != nil {
assert.Equal("one.com,two.org", marshal) t.Error(err)
}
if marshal != "one.com,two.org" {
t.Error("Expected MarshalFlag to provide \"one.com,two.org\", got", cds)
}
} }

View File

@ -7,7 +7,6 @@ import (
"fmt" "fmt"
"io" "io"
"io/ioutil" "io/ioutil"
"net/url"
"os" "os"
"regexp" "regexp"
"strconv" "strconv"
@ -24,34 +23,31 @@ 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:"Host for central auth login"`
Config func(s string) error `long:"config" env:"CONFIG" description:"Path to config file"` Config func(s string) error `long:"config" env:"CONFIG" description:"Config file"`
CookieDomains []CookieDomain `long:"cookie-domain" env:"COOKIE_DOMAIN" description:"Domain to set auth cookie on, can be set multiple times"` CookieDomains CookieDomains `long:"cookie-domains" env:"COOKIE_DOMAINS" description:"Comma separated list of cookie domains"`
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"`
Domains []string `long:"domain" env:"DOMAIN" description:"Only allow given email domains, can be set multiple times"` Domains CommaSeparatedList `long:"domains" env:"DOMAINS" description:"Comma separated list of email domains to allow"`
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"` 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)"`
Whitelist CommaSeparatedList `long:"whitelist" env:"WHITELIST" description:"Only allow given email addresses, can be set multiple times"` Whitelist CommaSeparatedList `long:"whitelist" env:"WHITELIST" description:"Comma separated list of email addresses to allow"`
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:"rules.<name>.<param>" description:"Rule definitions, param can be: \"action\" or \"rule\""` Rules map[string]*Rule `long:"rules.<name>.<param>" description:"Rule definitions, see docs, param can be: \"action\", \"rule\""`
// Filled during transformations // Filled during transformations
Secret []byte Secret []byte
Lifetime time.Duration Lifetime time.Duration
// Legacy // 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\""`
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\""` 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\""`
PromptLegacy string `long:"prompt" env:"PROMPT" description:"DEPRECATED - Use \"providers.google.prompt\""` PromptLegacy string `long:"prompt" env:"PROMPT" description:"DEPRECATED - Use \"providers.google.prompt\""`
CookieSecureLegacy string `long:"cookie-secure" env:"COOKIE_SECURE" namespace:"DERPS" description:"DEPRECATED - Use \"insecure-cookie\""`
} }
func NewGlobalConfig() Config { func NewGlobalConfig() Config {
@ -68,26 +64,6 @@ func NewGlobalConfig() Config {
func NewConfig(args []string) (Config, error) { func NewConfig(args []string) (Config, error) {
c := Config{ c := Config{
Rules: map[string]*Rule{}, Rules: map[string]*Rule{},
Providers: provider.Providers{
Google: provider.Google{
Scope: "https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email",
LoginURL: &url.URL{
Scheme: "https",
Host: "accounts.google.com",
Path: "/o/oauth2/auth",
},
TokenURL: &url.URL{
Scheme: "https",
Host: "www.googleapis.com",
Path: "/oauth2/v3/token",
},
UserURL: &url.URL{
Scheme: "https",
Host: "www.googleapis.com",
Path: "/oauth2/v2/userinfo",
},
},
},
} }
err := c.parseFlags(args) err := c.parseFlags(args)
@ -99,9 +75,6 @@ func NewConfig(args []string) (Config, error) {
// any further errors can be logged via logrus instead of printed? // any further errors can be logged via logrus instead of printed?
// Backwards compatability // Backwards compatability
if c.CookieSecretLegacy != "" && c.SecretString == "" {
c.SecretString = c.CookieSecretLegacy
}
if c.ClientIdLegacy != "" { if c.ClientIdLegacy != "" {
c.Providers.Google.ClientId = c.ClientIdLegacy c.Providers.Google.ClientId = c.ClientIdLegacy
} }
@ -118,17 +91,12 @@ func NewConfig(args []string) (Config, error) {
} }
c.InsecureCookie = !secure c.InsecureCookie = !secure
} }
if len(c.CookieDomainsLegacy) > 0 {
c.CookieDomains = append(c.CookieDomains, c.CookieDomainsLegacy...) // Provider defaults
} c.Providers.Google.Build()
if len(c.DomainsLegacy) > 0 {
c.Domains = append(c.Domains, c.DomainsLegacy...)
}
// Transformations // Transformations
if len(c.Path) > 0 && c.Path[0] != '/' { c.Path = fmt.Sprintf("/%s", c.Path)
c.Path = "/" + c.Path
}
c.Secret = []byte(c.SecretString) c.Secret = []byte(c.SecretString)
c.Lifetime = time.Second * time.Duration(c.LifetimeString) c.Lifetime = time.Second * time.Duration(c.LifetimeString)
@ -226,7 +194,7 @@ func handlFlagError(err error) error {
return err return err
} }
var legacyFileFormat = regexp.MustCompile(`(?m)^([a-z-]+) (.*)$`) var legacyFileFormat = regexp.MustCompile(`^([a-z-]+) ([\w\W]+)$`)
func convertLegacyToIni(name string) (io.Reader, error) { func convertLegacyToIni(name string) (io.Reader, error) {
b, err := ioutil.ReadFile(name) b, err := ioutil.ReadFile(name)
@ -282,12 +250,10 @@ func (r *Rule) Validate() {
} }
} }
// Legacy support for comma separated lists
type CommaSeparatedList []string type CommaSeparatedList []string
func (c *CommaSeparatedList) UnmarshalFlag(value string) error { func (c *CommaSeparatedList) UnmarshalFlag(value string) error {
*c = append(*c, strings.Split(value, ",")...) *c = strings.Split(value, ",")
return nil return nil
} }

View File

@ -1,13 +1,10 @@
package tfa package tfa
import ( import (
"net/url" "bytes"
"os" "os"
"testing" "testing"
"time" "time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
/** /**
@ -15,51 +12,56 @@ import (
*/ */
func TestConfigDefaults(t *testing.T) { func TestConfigDefaults(t *testing.T) {
assert := assert.New(t) // Check defaults
c, err := NewConfig([]string{}) c, err := NewConfig([]string{})
assert.Nil(err) if err != nil {
t.Error(err)
assert.Equal("warn", c.LogLevel)
assert.Equal("text", c.LogFormat)
assert.Equal("", c.AuthHost)
assert.Len(c.CookieDomains, 0)
assert.False(c.InsecureCookie)
assert.Equal("_forward_auth", c.CookieName)
assert.Equal("_forward_auth_csrf", c.CSRFCookieName)
assert.Equal("auth", c.DefaultAction)
assert.Len(c.Domains, 0)
assert.Equal(time.Second*time.Duration(43200), c.Lifetime)
assert.Equal("/_oauth", c.Path)
assert.Len(c.Whitelist, 0)
assert.Equal("https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email", c.Providers.Google.Scope)
assert.Equal("", c.Providers.Google.Prompt)
loginURL := &url.URL{
Scheme: "https",
Host: "accounts.google.com",
Path: "/o/oauth2/auth",
} }
assert.Equal(loginURL, c.Providers.Google.LoginURL)
tokenURL := &url.URL{ if c.LogLevel != "warn" {
Scheme: "https", t.Error("LogLevel default should be warn, got", c.LogLevel)
Host: "www.googleapis.com", }
Path: "/oauth2/v3/token", if c.LogFormat != "text" {
t.Error("LogFormat default should be text, got", c.LogFormat)
} }
assert.Equal(tokenURL, c.Providers.Google.TokenURL)
userURL := &url.URL{ if c.AuthHost != "" {
Scheme: "https", t.Error("AuthHost default should be empty, got", c.AuthHost)
Host: "www.googleapis.com", }
Path: "/oauth2/v2/userinfo", if len(c.CookieDomains) != 0 {
t.Error("CookieDomains default should be empty, got", c.CookieDomains)
}
if c.InsecureCookie != false {
t.Error("InsecureCookie default should be false, got", c.InsecureCookie)
}
if c.CookieName != "_forward_auth" {
t.Error("CookieName default should be _forward_auth, got", c.CookieName)
}
if c.CSRFCookieName != "_forward_auth_csrf" {
t.Error("CSRFCookieName default should be _forward_auth_csrf, got", c.CSRFCookieName)
}
if c.DefaultAction != "auth" {
t.Error("DefaultAction default should be auth, got", c.DefaultAction)
}
if len(c.Domains) != 0 {
t.Error("Domain default should be empty, got", c.Domains)
}
if c.Lifetime != time.Second*time.Duration(43200) {
t.Error("Lifetime default should be 43200, got", c.Lifetime)
}
if c.Path != "/_oauth" {
t.Error("Path default should be /_oauth, got", c.Path)
}
if len(c.Whitelist) != 0 {
t.Error("Whitelist default should be empty, got", c.Whitelist)
}
if c.Providers.Google.Prompt != "" {
t.Error("Providers.Google.Prompt default should be empty, got", c.Providers.Google.Prompt)
} }
assert.Equal(userURL, c.Providers.Google.UserURL)
} }
func TestConfigParseArgs(t *testing.T) { func TestConfigParseArgs(t *testing.T) {
assert := assert.New(t)
c, err := NewConfig([]string{ c, err := NewConfig([]string{
"--cookie-name=cookiename", "--cookie-name=cookiename",
"--csrf-cookie-name", "\"csrfcookiename\"", "--csrf-cookie-name", "\"csrfcookiename\"",
@ -68,152 +70,200 @@ func TestConfigParseArgs(t *testing.T) {
"--rule.two.action=auth", "--rule.two.action=auth",
"--rule.two.rule=\"Host(`two.com`) && Path(`/two`)\"", "--rule.two.rule=\"Host(`two.com`) && Path(`/two`)\"",
}) })
require.Nil(t, err) if err != nil {
t.Error(err)
}
// Check normal flags // Check normal flags
assert.Equal("cookiename", c.CookieName) if c.CookieName != "cookiename" {
assert.Equal("csrfcookiename", c.CSRFCookieName) t.Error("CookieName default should be cookiename, got", c.CookieName)
}
if c.CSRFCookieName != "csrfcookiename" {
t.Error("CSRFCookieName default should be csrfcookiename, got", c.CSRFCookieName)
}
// Check rules // Check rules
assert.Equal(map[string]*Rule{ if len(c.Rules) != 2 {
"1": { t.Error("Should create 2 rules, got:", len(c.Rules))
Action: "allow", }
Rule: "PathPrefix(`/one`)",
Provider: "google", // First rule
}, if rule, ok := c.Rules["1"]; !ok {
"two": { t.Error("Could not find rule key '1'")
Action: "auth", } else {
Rule: "Host(`two.com`) && Path(`/two`)", if rule.Action != "allow" {
Provider: "google", t.Error("First rule action should be allow, got:", rule.Action)
}, }
}, c.Rules) if rule.Rule != "PathPrefix(`/one`)" {
t.Error("First rule rule should be PathPrefix(`/one`), got:", rule.Rule)
}
if rule.Provider != "google" {
t.Error("First rule provider should be google, got:", rule.Provider)
}
}
// Second rule
if rule, ok := c.Rules["two"]; !ok {
t.Error("Could not find rule key '1'")
} else {
if rule.Action != "auth" {
t.Error("Second rule action should be auth, got:", rule.Action)
}
if rule.Rule != "Host(`two.com`) && Path(`/two`)" {
t.Error("Second rule rule should be Host(`two.com`) && Path(`/two`), got:", rule.Rule)
}
if rule.Provider != "google" {
t.Error("Second rule provider should be google, got:", rule.Provider)
}
}
} }
func TestConfigParseUnknownFlags(t *testing.T) { func TestConfigParseUnknownFlags(t *testing.T) {
_, err := NewConfig([]string{ _, err := NewConfig([]string{
"--unknown=_oauthpath2", "--unknown=_oauthpath2",
}) })
if assert.Error(t, err) { if err.Error() != "unknown flag: unknown" {
assert.Equal(t, "unknown flag: unknown", err.Error()) t.Error("Error should be \"unknown flag: unknown\", got:", err)
} }
} }
func TestConfigFlagBackwardsCompatability(t *testing.T) { func TestConfigFlagBackwardsCompatability(t *testing.T) {
assert := assert.New(t)
c, err := NewConfig([]string{ c, err := NewConfig([]string{
"--client-id=clientid", "--client-id=clientid",
"--client-secret=verysecret", "--client-secret=verysecret",
"--prompt=prompt", "--prompt=prompt",
"--cookie-secret=veryverysecret",
"--lifetime=200", "--lifetime=200",
"--cookie-secure=false", "--cookie-secure=false",
"--cookie-domains=test1.com,example.org",
"--cookie-domain=another1.net",
"--domains=test2.com,example.org",
"--domain=another2.net",
"--whitelist=test3.com,example.org",
"--whitelist=another3.net",
}) })
require.Nil(t, err) if err != nil {
t.Error(err)
// The following used to be passed as comma separated list
expected1 := []CookieDomain{
*NewCookieDomain("another1.net"),
*NewCookieDomain("test1.com"),
*NewCookieDomain("example.org"),
} }
assert.Equal(expected1, c.CookieDomains, "should read legacy comma separated list cookie-domains")
expected2 := []string{"another2.net", "test2.com", "example.org"} if c.ClientIdLegacy != "clientid" {
assert.Equal(expected2, c.Domains, "should read legacy comma separated list domains") t.Error("ClientIdLegacy should be clientid, got", c.ClientIdLegacy)
}
expected3 := CommaSeparatedList{"test3.com", "example.org", "another3.net"} if c.Providers.Google.ClientId != "clientid" {
assert.Equal(expected3, c.Whitelist, "should read legacy comma separated list whitelist") t.Error("Providers.Google.ClientId should be clientid, got", c.Providers.Google.ClientId)
}
// Name changed if c.ClientSecretLegacy != "verysecret" {
assert.Equal([]byte("veryverysecret"), c.Secret) t.Error("ClientSecretLegacy should be verysecret, got", c.ClientSecretLegacy)
}
// Google provider params used to be top level if c.Providers.Google.ClientSecret != "verysecret" {
assert.Equal("clientid", c.ClientIdLegacy) t.Error("Providers.Google.ClientSecret should be verysecret, got", c.Providers.Google.ClientSecret)
assert.Equal("clientid", c.Providers.Google.ClientId, "--client-id should set providers.google.client-id") }
assert.Equal("verysecret", c.ClientSecretLegacy) if c.PromptLegacy != "prompt" {
assert.Equal("verysecret", c.Providers.Google.ClientSecret, "--client-secret should set providers.google.client-secret") t.Error("PromptLegacy should be prompt, got", c.PromptLegacy)
assert.Equal("prompt", c.PromptLegacy) }
assert.Equal("prompt", c.Providers.Google.Prompt, "--prompt should set providers.google.promot") if c.Providers.Google.Prompt != "prompt" {
t.Error("Providers.Google.Prompt should be prompt, got", c.Providers.Google.Prompt)
}
// "cookie-secure" used to be a standard go bool flag that could take // "cookie-secure" used to be a standard go bool flag that could take
// true, TRUE, 1, false, FALSE, 0 etc. values. // true, TRUE, 1, false, FALSE, 0 etc. values.
// Here we're checking that format is still suppoted // Here we're checking that format is still suppoted
assert.Equal("false", c.CookieSecureLegacy) if c.CookieSecureLegacy != "false" || c.InsecureCookie != true {
assert.True(c.InsecureCookie, "--cookie-secure=false should set insecure-cookie true") t.Error("Setting cookie-secure=false should set InsecureCookie true, got", c.InsecureCookie)
}
c, err = NewConfig([]string{"--cookie-secure=TRUE"}) c, err = NewConfig([]string{"--cookie-secure=TRUE"})
assert.Nil(err) if err != nil {
assert.Equal("TRUE", c.CookieSecureLegacy) t.Error(err)
assert.False(c.InsecureCookie, "--cookie-secure=TRUE should set insecure-cookie false") }
if c.CookieSecureLegacy != "TRUE" || c.InsecureCookie != false {
t.Error("Setting cookie-secure=TRUE should set InsecureCookie false, got", c.InsecureCookie)
}
} }
func TestConfigParseIni(t *testing.T) { func TestConfigParseIni(t *testing.T) {
assert := assert.New(t)
c, err := NewConfig([]string{ c, err := NewConfig([]string{
"--config=../test/config0", "--config=../test/config0",
"--config=../test/config1", "--config=../test/config1",
"--csrf-cookie-name=csrfcookiename", "--csrf-cookie-name=csrfcookiename",
}) })
require.Nil(t, err) if err != nil {
t.Error(err)
}
assert.Equal("inicookiename", c.CookieName, "should be read from ini file") if c.CookieName != "inicookiename" {
assert.Equal("csrfcookiename", c.CSRFCookieName, "should be read from ini file") t.Error("CookieName should be read as inicookiename from ini file, got", c.CookieName)
assert.Equal("/two", c.Path, "variable in second ini file should override first ini file") }
if c.CSRFCookieName != "csrfcookiename" {
t.Error("CSRFCookieName argument should override ini file, got", c.CSRFCookieName)
}
if c.Path != "/two" {
t.Error("Path in second ini file should override first ini file, got", c.Path)
}
} }
func TestConfigFileBackwardsCompatability(t *testing.T) { func TestConfigFileBackwardsCompatability(t *testing.T) {
assert := assert.New(t)
c, err := NewConfig([]string{ c, err := NewConfig([]string{
"--config=../test/config-legacy", "--config=../test/config-legacy",
}) })
require.Nil(t, err) if err != nil {
t.Error(err)
}
assert.Equal("/two", c.Path, "variable in legacy config file should be read") if c.Path != "/two" {
assert.Equal("auth.legacy.com", c.AuthHost, "variable in legacy config file should be read") t.Error("Path in legacy config file should be read, got", c.Path)
}
} }
func TestConfigParseEnvironment(t *testing.T) { func TestConfigParseEnvironment(t *testing.T) {
assert := assert.New(t)
os.Setenv("COOKIE_NAME", "env_cookie_name") os.Setenv("COOKIE_NAME", "env_cookie_name")
c, err := NewConfig([]string{}) c, err := NewConfig([]string{})
assert.Nil(err) if err != nil {
t.Error(err)
}
assert.Equal("env_cookie_name", c.CookieName, "variable should be read from environment") if c.CookieName != "env_cookie_name" {
t.Error("CookieName should be read as env_cookie_name from environment, got", c.CookieName)
}
} }
func TestConfigTransformation(t *testing.T) { func TestConfigTransformation(t *testing.T) {
assert := assert.New(t)
c, err := NewConfig([]string{ c, err := NewConfig([]string{
"--url-path=_oauthpath", "--url-path=_oauthpath",
"--secret=verysecret", "--secret=verysecret",
"--lifetime=200", "--lifetime=200",
}) })
require.Nil(t, err) if err != nil {
t.Error(err)
}
assert.Equal("/_oauthpath", c.Path, "path should add slash to front") if c.Path != "/_oauthpath" {
t.Error("Path should add slash to front to get /_oauthpath, got:", c.Path)
}
assert.Equal("verysecret", c.SecretString) if c.SecretString != "verysecret" {
assert.Equal([]byte("verysecret"), c.Secret, "secret should be converted to byte array") t.Error("SecretString should be verysecret, got:", c.SecretString)
}
if bytes.Compare(c.Secret, []byte("verysecret")) != 0 {
t.Error("Secret should be []byte(verysecret), got:", string(c.Secret))
}
assert.Equal(200, c.LifetimeString) if c.LifetimeString != 200 {
assert.Equal(time.Second*time.Duration(200), c.Lifetime, "lifetime should be read and converted to duration") t.Error("LifetimeString should be 200, got:", c.LifetimeString)
}
if c.Lifetime != time.Second*time.Duration(200) {
t.Error("Lifetime default should be 200, got", c.Lifetime)
}
} }
func TestConfigCommaSeparatedList(t *testing.T) { func TestConfigCommaSeparatedList(t *testing.T) {
assert := assert.New(t)
list := CommaSeparatedList{} list := CommaSeparatedList{}
err := list.UnmarshalFlag("one,two") err := list.UnmarshalFlag("one,two")
assert.Nil(err) if err != nil {
assert.Equal(CommaSeparatedList{"one", "two"}, list, "should parse comma sepearated list") t.Error(err)
}
if len(list) != 2 || list[0] != "one" || list[1] != "two" {
t.Error("Expected UnmarshalFlag to provide CommaSeparatedList{one,two}, got", list)
}
marshal, err := list.MarshalFlag() marshal, err := list.MarshalFlag()
assert.Nil(err) if err != nil {
assert.Equal("one,two", marshal, "should marshal back to comma sepearated list") t.Error(err)
}
if marshal != "one,two" {
t.Error("Expected MarshalFlag to provide \"one,two\", got", list)
}
} }

View File

@ -18,6 +18,24 @@ type Google struct {
UserURL *url.URL UserURL *url.URL
} }
func (g *Google) Build() {
g.LoginURL = &url.URL{
Scheme: "https",
Host: "accounts.google.com",
Path: "/o/oauth2/auth",
}
g.TokenURL = &url.URL{
Scheme: "https",
Host: "www.googleapis.com",
Path: "/oauth2/v3/token",
}
g.UserURL = &url.URL{
Scheme: "https",
Host: "www.googleapis.com",
Path: "/oauth2/v2/userinfo",
}
}
func (g *Google) GetLoginURL(redirectUri, state string) string { func (g *Google) GetLoginURL(redirectUri, state string) string {
q := url.Values{} q := url.Values{}
q.Set("client_id", g.ClientId) q.Set("client_id", g.ClientId)

View File

@ -85,7 +85,6 @@ func (s *Server) AuthHandler() http.HandlerFunc {
// Forward them on // Forward them on
http.Redirect(w, r, GetLoginURL(r, nonce), http.StatusTemporaryRedirect) http.Redirect(w, r, GetLoginURL(r, nonce), http.StatusTemporaryRedirect)
logger.Debug("Done")
return return
} }
@ -168,7 +167,7 @@ func (s *Server) AuthCallbackHandler() http.HandlerFunc {
func (s *Server) logger(r *http.Request, msg string) *logrus.Entry { func (s *Server) logger(r *http.Request, msg string) *logrus.Entry {
// Create logger // Create logger
logger := log.WithFields(logrus.Fields{ logger := log.WithFields(logrus.Fields{
"SourceIP": r.Header.Get("X-Forwarded-For"), "RemoteAddr": r.RemoteAddr,
}) })
// Log request // Log request

View File

@ -8,12 +8,8 @@ import (
"net/url" "net/url"
"strings" "strings"
"testing" "testing"
"github.com/stretchr/testify/assert"
) )
// TODO:
/** /**
* Setup * Setup
*/ */
@ -28,18 +24,19 @@ func init() {
*/ */
func TestServerAuthHandler(t *testing.T) { func TestServerAuthHandler(t *testing.T) {
assert := assert.New(t)
config, _ = NewConfig([]string{}) config, _ = NewConfig([]string{})
// Should redirect vanilla request to login url // Should redirect vanilla request to login url
req := newHttpRequest("/foo") req := newHttpRequest("/foo")
res, _ := doHttpRequest(req, nil)
assert.Equal(307, res.StatusCode, "vanilla request should be redirected")
res, _ := httpRequest(req, nil)
if res.StatusCode != 307 {
t.Error("Vanilla request should be redirected with 307, got:", res.StatusCode)
}
fwd, _ := res.Location() fwd, _ := res.Location()
assert.Equal("https", fwd.Scheme, "vanilla request should be redirected to google") if fwd.Scheme != "https" || fwd.Host != "accounts.google.com" || fwd.Path != "/o/oauth2/auth" {
assert.Equal("accounts.google.com", fwd.Host, "vanilla request should be redirected to google") t.Error("Vanilla request should be redirected to login url, got:", fwd)
assert.Equal("/o/oauth2/auth", fwd.Path, "vanilla request should be redirected to google") }
// Should catch invalid cookie // Should catch invalid cookie
req = newHttpRequest("/foo") req = newHttpRequest("/foo")
@ -47,33 +44,42 @@ func TestServerAuthHandler(t *testing.T) {
parts := strings.Split(c.Value, "|") parts := strings.Split(c.Value, "|")
c.Value = fmt.Sprintf("bad|%s|%s", parts[1], parts[2]) c.Value = fmt.Sprintf("bad|%s|%s", parts[1], parts[2])
res, _ = doHttpRequest(req, c) res, _ = httpRequest(req, c)
assert.Equal(401, res.StatusCode, "invalid cookie should not be authorised") if res.StatusCode != 401 {
t.Error("Request with invalid cookie shound't be authorised", res.StatusCode)
}
// Should validate email // Should validate email
req = newHttpRequest("/foo") req = newHttpRequest("/foo")
c = MakeCookie(req, "test@example.com") c = MakeCookie(req, "test@example.com")
config.Domains = []string{"test.com"} config.Domains = []string{"test.com"}
res, _ = doHttpRequest(req, c) res, _ = httpRequest(req, c)
assert.Equal(401, res.StatusCode, "invalid email should not be authorised") if res.StatusCode != 401 {
t.Error("Request with invalid email shound't be authorised", res.StatusCode)
}
// Should allow valid request email // Should allow valid request email
req = newHttpRequest("/foo") req = newHttpRequest("/foo")
c = MakeCookie(req, "test@example.com") c = MakeCookie(req, "test@example.com")
config.Domains = []string{} config.Domains = []string{}
res, _ = doHttpRequest(req, c) res, _ = httpRequest(req, c)
assert.Equal(200, res.StatusCode, "valid request should be allowed") if res.StatusCode != 200 {
t.Error("Valid request should be allowed, got:", res.StatusCode)
}
// Should pass through user // Should pass through user
users := res.Header["X-Forwarded-User"] users := res.Header["X-Forwarded-User"]
assert.Len(users, 1, "valid request should have X-Forwarded-User header") if len(users) != 1 {
assert.Equal([]string{"test@example.com"}, users, "X-Forwarded-User header should match user") t.Error("Valid request missing X-Forwarded-User header")
} else if users[0] != "test@example.com" {
t.Error("X-Forwarded-User should match user, got: ", users)
}
} }
func TestServerAuthCallback(t *testing.T) { func TestServerAuthCallback(t *testing.T) {
assert := assert.New(t)
config, _ = NewConfig([]string{}) config, _ = NewConfig([]string{})
// Setup token server // Setup token server
@ -92,43 +98,50 @@ func TestServerAuthCallback(t *testing.T) {
// Should pass auth response request to callback // Should pass auth response request to callback
req := newHttpRequest("/_oauth") req := newHttpRequest("/_oauth")
res, _ := doHttpRequest(req, nil) res, _ := httpRequest(req, nil)
assert.Equal(401, res.StatusCode, "auth callback without cookie shouldn't be authorised") if res.StatusCode != 401 {
t.Error("Auth callback without cookie shound't be authorised, got:", res.StatusCode)
}
// Should catch invalid csrf cookie // Should catch invalid csrf cookie
req = newHttpRequest("/_oauth?state=12345678901234567890123456789012:http://redirect") req = newHttpRequest("/_oauth?state=12345678901234567890123456789012:http://redirect")
c := MakeCSRFCookie(req, "nononononononononononononononono") c := MakeCSRFCookie(req, "nononononononononononononononono")
res, _ = doHttpRequest(req, c) res, _ = httpRequest(req, c)
assert.Equal(401, res.StatusCode, "auth callback with invalid cookie shouldn't be authorised") if res.StatusCode != 401 {
t.Error("Auth callback with invalid cookie shound't be authorised, got:", res.StatusCode)
}
// Should redirect valid request // Should redirect valid request
req = newHttpRequest("/_oauth?state=12345678901234567890123456789012:http://redirect") req = newHttpRequest("/_oauth?state=12345678901234567890123456789012:http://redirect")
c = MakeCSRFCookie(req, "12345678901234567890123456789012") c = MakeCSRFCookie(req, "12345678901234567890123456789012")
res, _ = doHttpRequest(req, c) res, _ = httpRequest(req, c)
assert.Equal(307, res.StatusCode, "valid auth callback should be allowed") if res.StatusCode != 307 {
t.Error("Valid callback should be allowed, got:", res.StatusCode)
}
fwd, _ := res.Location() fwd, _ := res.Location()
assert.Equal("http", fwd.Scheme, "valid request should be redirected to return url") if fwd.Scheme != "http" || fwd.Host != "redirect" || fwd.Path != "" {
assert.Equal("redirect", fwd.Host, "valid request should be redirected to return url") t.Error("Valid request should be redirected to return url, got:", fwd)
assert.Equal("", fwd.Path, "valid request should be redirected to return url") }
} }
func TestServerDefaultAction(t *testing.T) { func TestServerDefaultAction(t *testing.T) {
assert := assert.New(t)
config, _ = NewConfig([]string{}) config, _ = NewConfig([]string{})
req := newHttpRequest("/random") req := newHttpRequest("/random")
res, _ := doHttpRequest(req, nil) res, _ := httpRequest(req, nil)
assert.Equal(307, res.StatusCode, "request should require auth with auth default handler") if res.StatusCode != 307 {
t.Error("Request should require auth with auth default handler, got:", res.StatusCode)
}
config.DefaultAction = "allow" config.DefaultAction = "allow"
req = newHttpRequest("/random") req = newHttpRequest("/random")
res, _ = doHttpRequest(req, nil) res, _ = httpRequest(req, nil)
assert.Equal(200, res.StatusCode, "request should be allowed with default handler") if res.StatusCode != 200 {
t.Error("Request should be allowed with allow default handler, got:", res.StatusCode)
}
} }
func TestServerRoutePathPrefix(t *testing.T) { func TestServerRoutePathPrefix(t *testing.T) {
assert := assert.New(t)
config, _ = NewConfig([]string{}) config, _ = NewConfig([]string{})
config.Rules = map[string]*Rule{ config.Rules = map[string]*Rule{
"web1": { "web1": {
@ -139,13 +152,17 @@ func TestServerRoutePathPrefix(t *testing.T) {
// Should block any request // Should block any request
req := newHttpRequest("/random") req := newHttpRequest("/random")
res, _ := doHttpRequest(req, nil) res, _ := httpRequest(req, nil)
assert.Equal(307, res.StatusCode, "request not matching any rule should require auth") if res.StatusCode != 307 {
t.Error("Request not matching any rule should require auth, got:", res.StatusCode)
}
// Should allow /api request // Should allow /api request
req = newHttpRequest("/api") req = newHttpRequest("/api")
res, _ = doHttpRequest(req, nil) res, _ = httpRequest(req, nil)
assert.Equal(200, res.StatusCode, "request matching allow rule should be allowed") if res.StatusCode != 200 {
t.Error("Request matching allowed rule should be allowed, got:", res.StatusCode)
}
} }
/** /**
@ -169,7 +186,7 @@ func (t *UserServerHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}`) }`)
} }
func doHttpRequest(r *http.Request, c *http.Cookie) (*http.Response, string) { func httpRequest(r *http.Request, c *http.Cookie) (*http.Response, string) {
w := httptest.NewRecorder() w := httptest.NewRecorder()
// Set cookies on recorder // Set cookies on recorder
@ -182,6 +199,7 @@ func doHttpRequest(r *http.Request, c *http.Cookie) (*http.Response, string) {
r.Header.Add("Cookie", c) r.Header.Add("Cookie", c)
} }
NewServer().RootHandler(w, r) NewServer().RootHandler(w, r)
res := w.Result() res := w.Result()

View File

@ -1,2 +1 @@
url-path two url-path two
auth-host auth.legacy.com