Compare commits
63 Commits
v2.0.0-rc2
...
master
Author | SHA1 | Date | |
---|---|---|---|
3c20d31a13
|
|||
14a1cb8ea3
|
|||
506fdc56ad
|
|||
2ddbb8576f
|
|||
e41e8e1a17
|
|||
86894f72ed
|
|||
3f9d70e87b
|
|||
fb24320552
|
|||
f6120640d2
|
|||
ab2d527dbd
|
|||
5828a9a5a2
|
|||
c4317b7503 | |||
4ffb6593d5 | |||
6c6f75e80d | |||
8be8244b13 | |||
f96a3fb332 | |||
c19f622fbd | |||
04f5499f0b | |||
41560feaa7 | |||
1743537438 | |||
9e5994b959 | |||
870724c994 | |||
be2b4ba9f4 | |||
529e28d83b | |||
2937b04fdb | |||
fb8b216481 | |||
8b3a950162 | |||
655eddeaf9 | |||
c63fd738d6 | |||
00b5d9e031 | |||
8902cf8735 | |||
3345f8ec69 | |||
60604ad3db | |||
a668454a11 | |||
eec62eb03a | |||
7381450015 | |||
f7a94e7db9 | |||
f802a366de | |||
07f9587bc1 | |||
1ac0ca9732 | |||
9abf5645b7 | |||
3a66191314 | |||
c3b4ba8244 | |||
b413c60d42 | |||
e678a33016 | |||
3652a0b244 | |||
68c329901a | |||
ffa5afbf22 | |||
5a9c6adedf | |||
5dfd4f2878 | |||
a99330e6b2 | |||
5a676f3068 | |||
3e6ccc8f45 | |||
3e92400202 | |||
72fc88a82b | |||
2c148d3a23 | |||
d33ecc0654 | |||
41a3f2a5a9 | |||
5a17187855 | |||
e7b567bc92 | |||
a4a34dcd76 | |||
d1b12e4ffb | |||
6f3ac5efe5 |
@ -1,2 +1,4 @@
|
|||||||
example
|
example
|
||||||
.travis.yml
|
.travis.yml
|
||||||
|
.git
|
||||||
|
.gitlab-ci.yml
|
||||||
|
6
.gitlab-ci.yml
Normal file
6
.gitlab-ci.yml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
include:
|
||||||
|
- project: dockerized/commons
|
||||||
|
ref: master
|
||||||
|
file: gitlab-ci-template.yml
|
||||||
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
|||||||
language: go
|
|
||||||
sudo: false
|
|
||||||
go:
|
|
||||||
- "1.12"
|
|
||||||
script: env GO111MODULE=on go test -v ./...
|
|
@ -1,4 +1,4 @@
|
|||||||
FROM golang:1.12-alpine as builder
|
FROM golang:1.13-alpine as builder
|
||||||
|
|
||||||
# Setup
|
# Setup
|
||||||
RUN mkdir -p /go/src/github.com/thomseddon/traefik-forward-auth
|
RUN mkdir -p /go/src/github.com/thomseddon/traefik-forward-auth
|
||||||
@ -9,7 +9,7 @@ RUN apk add --no-cache git
|
|||||||
|
|
||||||
# Copy & build
|
# Copy & build
|
||||||
ADD . /go/src/github.com/thomseddon/traefik-forward-auth/
|
ADD . /go/src/github.com/thomseddon/traefik-forward-auth/
|
||||||
RUN CGO_ENABLED=0 GOOS=linux GO111MODULE=on go build -a -installsuffix nocgo -o /traefik-forward-auth github.com/thomseddon/traefik-forward-auth/cmd
|
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GO111MODULE=on go build -a -installsuffix nocgo -o /traefik-forward-auth github.com/thomseddon/traefik-forward-auth/cmd
|
||||||
|
|
||||||
# Copy into scratch container
|
# Copy into scratch container
|
||||||
FROM scratch
|
FROM scratch
|
||||||
|
@ -2,6 +2,8 @@ MIT License
|
|||||||
|
|
||||||
Copyright (c) [2018] [Thom Seddon]
|
Copyright (c) [2018] [Thom Seddon]
|
||||||
|
|
||||||
|
Copyright (c) [2023] [Wolfgang Hottgenroth]
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
in the Software without restriction, including without limitation the rights
|
in the Software without restriction, including without limitation the rights
|
||||||
|
7
Makefile
7
Makefile
@ -1,5 +1,8 @@
|
|||||||
|
|
||||||
format:
|
format:
|
||||||
gofmt -w -s internal/*.go cmd/*.go
|
gofmt -w -s internal/*.go internal/provider/*.go cmd/*.go
|
||||||
|
|
||||||
.PHONY: format
|
test:
|
||||||
|
go test -v ./...
|
||||||
|
|
||||||
|
.PHONY: format test
|
||||||
|
326
README.md
326
README.md
@ -1,332 +1,48 @@
|
|||||||
|
# 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/)
|
Yet another minimal modification of a great minimal forward authentication service that provides OAuth/SSO login and authentication for the [traefik](https://github.com/containous/traefik) reverse proxy/load balancer.
|
||||||
|
|
||||||
|
|
||||||
A minimal forward authentication service that provides 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))
|
The original [traefik-forward-auth](https://github.com/thomseddon/traefik-forward-auth) provides the forwarding of authentication between an Identity Provider like [keycloak](https://www.keycloak.org/) and the [ForwardAuth](https://doc.traefik.io/traefik/middlewares/http/forwardauth/) middleware of [traefik](https://doc.traefik.io/traefik/).
|
||||||
- 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))
|
|
||||||
|
|
||||||
# Contents
|
The modification of this project is to add minimal authorization functionality. The [traefik-forward-auth](https://home.hottis.de/gitlab/dockerized/traefik-forward-auth/) is configured with a `REQUIRED_ROLE` and access to the resource is only granted if the access token issued by the Identity Provider contains a claim with that particular role.
|
||||||
|
|
||||||
- [Usage](#usage)
|
## Docker Image
|
||||||
- [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 Docker image can be found at [DockerHub wollud1969/traefik-forward-auth](https://hub.docker.com/r/wollud1969/traefik-forward-auth).
|
||||||
|
|
||||||
#### 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
|
### ... of traefik-forward-auth
|
||||||
|
|
||||||
The following configuration options are supported:
|
In the `examples` directory the ymls to deploy a whoami service ([at GitHub](https://github.com/traefik/whoami/), [at Docker Hub](https://hub.docker.com/r/containous/whoami)) and the related ymls to deploy and configure the traefik-forward-auth service.
|
||||||
|
The only relevant modification to the original [advanced separate pod example](https://github.com/thomseddon/traefik-forward-auth/tree/master/examples/traefik-v2/kubernetes/advanced-separate-pod) is the configuration parameter `REQUIRED_ROLE`.
|
||||||
|
|
||||||
```
|
### ... of the Identity Provider
|
||||||
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
|
|
||||||
|
|
||||||
Note, if you pass `whitelist` then only this is checked and `domain` is effectively ignored.
|
|
||||||
|
|
||||||
### Forwarded Headers
|
|
||||||
|
|
||||||
The authenticated user is set in the `X-Forwarded-User` header, to pass this on add this to the `authResponseHeaders` config option in traefik, as shown [here](https://github.com/thomseddon/traefik-forward-auth/blob/master/examples/docker-compose-dev.yml).
|
|
||||||
|
|
||||||
### Operation Modes
|
|
||||||
|
|
||||||
#### Overlay Mode
|
|
||||||
|
|
||||||
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:
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
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 Mode
|
|
||||||
|
|
||||||
This is an optional mode of operation that is useful when dealing with a large number of subdomains, it is activated by using the `auth-host` config option (see [this example docker-compose.yml](https://github.com/thomseddon/traefik-forward-auth/blob/master/examples/docker-compose-auth-host.yml)).
|
|
||||||
|
|
||||||
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`.
|
|
||||||
|
|
||||||
The user flow will then be:
|
|
||||||
|
|
||||||
1. Request to `app10.test.com/home/page`
|
|
||||||
2. User redirected to Google login
|
|
||||||
3. After Google login, user is redirected to `auth.test.com/_oauth`
|
|
||||||
4. Token, user and CSRF cookie is validated, auth cookie is set to `test.com`
|
|
||||||
5. User is redirected to `app10.test.com/home/page`
|
|
||||||
6. Request is allowed
|
|
||||||
|
|
||||||
With this setup, only `auth.test.com` must be permitted in the Google console.
|
|
||||||
|
|
||||||
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`
|
|
||||||
|
|
||||||
## Copyright
|
## Copyright
|
||||||
|
|
||||||
2018 Thom Seddon
|
2018 Thom Seddon
|
||||||
|
|
||||||
|
2023 Wolfgang Hottgenroth
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
[MIT](https://github.com/thomseddon/traefik-forward-auth/blob/master/LICENSE.md)
|
[MIT](https://github.com/thomseddon/traefik-forward-auth/blob/master/LICENSE.md)
|
||||||
|
|
||||||
|
[MIT](https://home.hottis.de/gitlab/dockerized/traefik-forward-auth/-/blob/master/LICENSE.md)
|
||||||
|
|
||||||
|
|
||||||
|
515
README.thomseddon.md
Normal file
515
README.thomseddon.md
Normal file
@ -0,0 +1,515 @@
|
|||||||
|
|
||||||
|
# Traefik Forward Auth  [](https://goreportcard.com/report/github.com/thomseddon/traefik-forward-auth)  [](https://GitHub.com/thomseddon/traefik-forward-auth/releases/)
|
||||||
|
|
||||||
|
|
||||||
|
A minimal forward authentication service that provides OAuth/SSO login and authentication for the [traefik](https://github.com/containous/traefik) reverse proxy/load balancer.
|
||||||
|
|
||||||
|
## Why?
|
||||||
|
|
||||||
|
- Seamlessly overlays any http service with a single endpoint (see: `url-path` in [Configuration](#configuration))
|
||||||
|
- Supports multiple providers including Google and OpenID Connect (supported by Azure, Github, Salesforce etc.)
|
||||||
|
- Supports multiple 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))
|
||||||
|
|
||||||
|
# Contents
|
||||||
|
|
||||||
|
- [Releases](#releases)
|
||||||
|
- [Usage](#usage)
|
||||||
|
- [Simple](#simple)
|
||||||
|
- [Advanced](#advanced)
|
||||||
|
- [Provider Setup](#provider-setup)
|
||||||
|
- [Configuration](#configuration)
|
||||||
|
- [Overview](#overview)
|
||||||
|
- [Option Details](#option-details)
|
||||||
|
- [Concepts](#concepts)
|
||||||
|
- [Forwarded Headers](#forwarded-headers)
|
||||||
|
- [User Restriction](#user-restriction)
|
||||||
|
- [Applying Authentication](#applying-authentication)
|
||||||
|
- [Global Authentication](#global-authentication)
|
||||||
|
- [Selective Ingress Authentication in Kubernetes](#selective-ingress-authentication-in-kubernetes)
|
||||||
|
- [Selective Container Authentication in Swarm](#selective-container-authentication-in-swarm)
|
||||||
|
- [Rules Based Authentication](#rules-based-authentication)
|
||||||
|
- [Operation Modes](#operation-modes)
|
||||||
|
- [Overlay Mode](#overlay-mode)
|
||||||
|
- [Auth Host Mode](#auth-host-mode)
|
||||||
|
- [Logging Out](#logging-out)
|
||||||
|
- [Copyright](#copyright)
|
||||||
|
- [License](#license)
|
||||||
|
|
||||||
|
## Releases
|
||||||
|
|
||||||
|
We recommend using the `2` tag on docker hub (`thomseddon/traefik-forward-auth:2`).
|
||||||
|
|
||||||
|
You can also use the latest incremental releases found on [docker hub](https://hub.docker.com/r/thomseddon/traefik-forward-auth/tags) and [github](https://github.com/thomseddon/traefik-forward-auth/releases).
|
||||||
|
|
||||||
|
ARM releases are also available on docker hub, just append `-arm` or `-arm64` to your desired released (e.g. `2-arm` or `2.1-arm64`).
|
||||||
|
|
||||||
|
We also build binary files for usage without docker starting with releases after 2.2.0 You can find these as assets of the specific GitHub release.
|
||||||
|
|
||||||
|
#### Upgrade Guide
|
||||||
|
|
||||||
|
v2 was released in June 2019, whilst this is fully backwards compatible, a number of configuration options were modified, please see the [upgrade guide](https://github.com/thomseddon/traefik-forward-auth/wiki/v2-Upgrade-Guide) to prevent warnings on startup and ensure you are using the current configuration.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
#### Simple:
|
||||||
|
|
||||||
|
See below for instructions on how to setup your [Provider Setup](#provider-setup).
|
||||||
|
|
||||||
|
docker-compose.yml:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
version: '3'
|
||||||
|
|
||||||
|
services:
|
||||||
|
traefik:
|
||||||
|
image: traefik:v2.2
|
||||||
|
command: --providers.docker
|
||||||
|
ports:
|
||||||
|
- "8085:80"
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
|
||||||
|
traefik-forward-auth:
|
||||||
|
image: thomseddon/traefik-forward-auth:2
|
||||||
|
environment:
|
||||||
|
- PROVIDERS_GOOGLE_CLIENT_ID=your-client-id
|
||||||
|
- PROVIDERS_GOOGLE_CLIENT_SECRET=your-client-secret
|
||||||
|
- SECRET=something-random
|
||||||
|
- INSECURE_COOKIE=true # Example assumes no https, do not use in production
|
||||||
|
labels:
|
||||||
|
- "traefik.http.middlewares.traefik-forward-auth.forwardauth.address=http://traefik-forward-auth:4181"
|
||||||
|
- "traefik.http.middlewares.traefik-forward-auth.forwardauth.authResponseHeaders=X-Forwarded-User"
|
||||||
|
- "traefik.http.services.traefik-forward-auth.loadbalancer.server.port=4181"
|
||||||
|
|
||||||
|
whoami:
|
||||||
|
image: containous/whoami
|
||||||
|
labels:
|
||||||
|
- "traefik.http.routers.whoami.rule=Host(`whoami.mycompany.com`)"
|
||||||
|
- "traefik.http.routers.whoami.middlewares=traefik-forward-auth"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Advanced:
|
||||||
|
|
||||||
|
Please see the examples directory for a more complete [docker-compose.yml](https://github.com/thomseddon/traefik-forward-auth/blob/master/examples/traefik-v2/swarm/docker-compose.yml) or [kubernetes/simple-separate-pod](https://github.com/thomseddon/traefik-forward-auth/blob/master/examples/traefik-v2/kubernetes/simple-separate-pod/).
|
||||||
|
|
||||||
|
Also in the examples directory is [docker-compose-auth-host.yml](https://github.com/thomseddon/traefik-forward-auth/blob/master/examples/traefik-v2/swarm/docker-compose-auth-host.yml) and [kubernetes/advanced-separate-pod](https://github.com/thomseddon/traefik-forward-auth/blob/master/examples/traefik-v2/kubernetes/advanced-separate-pod/) which shows how to configure a central auth host, along with some other options.
|
||||||
|
|
||||||
|
#### Provider Setup
|
||||||
|
|
||||||
|
Below are some general notes on provider setup, specific instructions and examples for a number of providers can be found on the [Provider Setup](https://github.com/thomseddon/traefik-forward-auth/wiki/Provider-Setup) wiki page.
|
||||||
|
|
||||||
|
##### Google
|
||||||
|
|
||||||
|
Head to https://console.developers.google.com and make sure you've switched to the correct email account.
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
You must set the `providers.google.client-id` and `providers.google.client-secret` config options.
|
||||||
|
|
||||||
|
##### OpenID Connect
|
||||||
|
|
||||||
|
Any provider that supports OpenID Connect 1.0 can be configured via the OIDC config options below.
|
||||||
|
|
||||||
|
You must set the `providers.oidc.issuer-url`, `providers.oidc.client-id` and `providers.oidc.client-secret` config options.
|
||||||
|
|
||||||
|
Please see the [Provider Setup](https://github.com/thomseddon/traefik-forward-auth/wiki/Provider-Setup) wiki page for examples.
|
||||||
|
|
||||||
|
##### Generic OAuth2
|
||||||
|
|
||||||
|
For providers that don't support OpenID Connect, we also have the Generic OAuth2 provider where you can statically configure the OAuth2 and "user" endpoints.
|
||||||
|
|
||||||
|
You must set:
|
||||||
|
- `providers.generic-oauth.auth-url` - URL the client should be sent to authenticate the authenticate
|
||||||
|
- `providers.generic-oauth.token-url` - URL the service should call to exchange an auth code for an access token
|
||||||
|
- `providers.generic-oauth.user-url` - URL used to retrieve user info (service makes a GET request)
|
||||||
|
- `providers.generic-oauth.client-id` - Client ID
|
||||||
|
- `providers.generic-oauth.client-secret` - Client Secret
|
||||||
|
|
||||||
|
You can also set:
|
||||||
|
- `providers.generic-oauth.scope`- Any scopes that should be included in the request (default: profile, email)
|
||||||
|
- `providers.generic-oauth.token-style` - How token is presented when querying the User URL. Can be `header` or `query`, defaults to `header`. With `header` the token is provided in an Authorization header, with query the token is provided in the `access_token` query string value.
|
||||||
|
|
||||||
|
Please see the [Provider Setup](https://github.com/thomseddon/traefik-forward-auth/wiki/Provider-Setup) wiki page for examples.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Overview
|
||||||
|
|
||||||
|
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]
|
||||||
|
--default-provider=[google|oidc|generic-oauth] Default provider (default: google) [$DEFAULT_PROVIDER]
|
||||||
|
--domain= Only allow given email domains, can be set multiple times [$DOMAIN]
|
||||||
|
--lifetime= Lifetime in seconds (default: 43200) [$LIFETIME]
|
||||||
|
--logout-redirect= URL to redirect to following logout [$LOGOUT_REDIRECT]
|
||||||
|
--url-path= Callback URL Path (default: /_oauth) [$URL_PATH]
|
||||||
|
--secret= Secret used for signing (required) [$SECRET]
|
||||||
|
--whitelist= Only allow given email addresses, can be set multiple times [$WHITELIST]
|
||||||
|
--port= Port to listen on (default: 4181) [$PORT]
|
||||||
|
--rule.<name>.<param>= Rule definitions, param can be: "action", "rule" or "provider"
|
||||||
|
|
||||||
|
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]
|
||||||
|
|
||||||
|
OIDC Provider:
|
||||||
|
--providers.oidc.issuer-url= Issuer URL [$PROVIDERS_OIDC_ISSUER_URL]
|
||||||
|
--providers.oidc.client-id= Client ID [$PROVIDERS_OIDC_CLIENT_ID]
|
||||||
|
--providers.oidc.client-secret= Client Secret [$PROVIDERS_OIDC_CLIENT_SECRET]
|
||||||
|
--providers.oidc.resource= Optional resource indicator [$PROVIDERS_OIDC_RESOURCE]
|
||||||
|
|
||||||
|
Generic OAuth2 Provider:
|
||||||
|
--providers.generic-oauth.auth-url= Auth/Login URL [$PROVIDERS_GENERIC_OAUTH_AUTH_URL]
|
||||||
|
--providers.generic-oauth.token-url= Token URL [$PROVIDERS_GENERIC_OAUTH_TOKEN_URL]
|
||||||
|
--providers.generic-oauth.user-url= URL used to retrieve user info [$PROVIDERS_GENERIC_OAUTH_USER_URL]
|
||||||
|
--providers.generic-oauth.client-id= Client ID [$PROVIDERS_GENERIC_OAUTH_CLIENT_ID]
|
||||||
|
--providers.generic-oauth.client-secret= Client Secret [$PROVIDERS_GENERIC_OAUTH_CLIENT_SECRET]
|
||||||
|
--providers.generic-oauth.scope= Scopes (default: profile, email) [$PROVIDERS_GENERIC_OAUTH_SCOPE]
|
||||||
|
--providers.generic-oauth.token-style=[header|query] How token is presented when querying the User URL (default: header)
|
||||||
|
[$PROVIDERS_GENERIC_OAUTH_TOKEN_STYLE]
|
||||||
|
--providers.generic-oauth.resource= Optional resource indicator [$PROVIDERS_GENERIC_OAUTH_RESOURCE]
|
||||||
|
|
||||||
|
Help Options:
|
||||||
|
-h, --help Show this help message
|
||||||
|
```
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
- `default-provider`
|
||||||
|
|
||||||
|
Set the default provider to use for authentication, this can be overridden within [rules](#rules). Valid options are currently `google` or `oidc`.
|
||||||
|
|
||||||
|
Default: `google`
|
||||||
|
|
||||||
|
- `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)
|
||||||
|
|
||||||
|
- `logout-redirect`
|
||||||
|
|
||||||
|
When set, users will be redirected to this URL following logout.
|
||||||
|
|
||||||
|
- `match-whitelist-or-domain`
|
||||||
|
|
||||||
|
When enabled, users will be permitted if they match *either* the `whitelist` or `domain` parameters.
|
||||||
|
|
||||||
|
This will be enabled by default in v3, but is disabled by default in v2 to maintain backwards compatibility.
|
||||||
|
|
||||||
|
Default: `false`
|
||||||
|
|
||||||
|
For more details, please also read [User Restriction](#user-restriction) in the concepts section.
|
||||||
|
|
||||||
|
- `url-path`
|
||||||
|
|
||||||
|
Customise the path that this service uses to handle the callback following authentication.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
- `rule`
|
||||||
|
|
||||||
|
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`
|
||||||
|
- `domains` - optional, same usage as [`domain`](#domain)
|
||||||
|
- `provider` - same usage as [`default-provider`](#default-provider), supported values:
|
||||||
|
- `google`
|
||||||
|
- `oidc`
|
||||||
|
- `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`)``
|
||||||
|
- `whitelist` - optional, same usage as whitelist`](#whitelist)
|
||||||
|
|
||||||
|
For example:
|
||||||
|
```
|
||||||
|
# Allow requests that being with `/api/public` and contain the `Content-Type` header with a value of `application/json`
|
||||||
|
rule.1.action = allow
|
||||||
|
rule.1.rule = PathPrefix(`/api/public`) && Headers(`Content-Type`, `application/json`)
|
||||||
|
|
||||||
|
# Allow requests that have the exact path `/public`
|
||||||
|
rule.two.action = allow
|
||||||
|
rule.two.rule = Path(`/public`)
|
||||||
|
|
||||||
|
# Use OpenID Connect provider (must be configured) for requests that begin with `/github`
|
||||||
|
rule.oidc.action = auth
|
||||||
|
rule.oidc.provider = oidc
|
||||||
|
rule.oidc.rule = PathPrefix(`/github`)
|
||||||
|
|
||||||
|
# Allow jane@example.com to `/janes-eyes-only`
|
||||||
|
rule.two.action = allow
|
||||||
|
rule.two.rule = Path(`/janes-eyes-only`)
|
||||||
|
rule.two.whitelist = jane@example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: It is possible to break your redirect flow with rules, please be careful not to create an `allow` rule that matches your redirect_uri unless you know what you're doing. This limitation is being tracked in in #101 and the behaviour will change in future releases.
|
||||||
|
|
||||||
|
## Concepts
|
||||||
|
|
||||||
|
### 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
|
||||||
|
|
||||||
|
Note, if you pass both `whitelist` and `domain`, then the default behaviour is for only `whitelist` to be used and `domain` will be effectively ignored. You can allow users matching *either* `whitelist` or `domain` by passing the `match-whitelist-or-domain` parameter (this will be the default behaviour in v3). If you set `domains` or `whitelist` on a rule, the global configuration is ignored.
|
||||||
|
|
||||||
|
### Forwarded Headers
|
||||||
|
|
||||||
|
The authenticated user is set in the `X-Forwarded-User` header, to pass this on add this to the `authResponseHeaders` config option in traefik, as shown below in the [Applying Authentication](#applying-authentication) section.
|
||||||
|
|
||||||
|
### Applying Authentication
|
||||||
|
|
||||||
|
Authentication can be applied in a variety of ways, either globally across all requests, or selectively to specific containers/ingresses.
|
||||||
|
|
||||||
|
#### Global Authentication
|
||||||
|
|
||||||
|
This can be achieved by enabling forward authentication for an entire entrypoint, for example, with http only:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
--entryPoints.http.address=:80
|
||||||
|
--entrypoints.http.http.middlewares=traefik-forward-auth # "default-traefik-forward-auth" on kubernetes
|
||||||
|
```
|
||||||
|
|
||||||
|
Or https:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
--entryPoints.http.address=:80
|
||||||
|
--entryPoints.http.http.redirections.entryPoint.to=https
|
||||||
|
--entryPoints.http.http.redirections.entryPoint.scheme=https
|
||||||
|
--entryPoints.https.address=:443
|
||||||
|
--entrypoints.https.http.middlewares=traefik-forward-auth # "default-traefik-forward-auth" on kubernetes
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: Traefik prepends the namespace to the name of middleware defined via a kubernetes resource. This is handled automatically when referencing the middleware from another resource in the same namespace (so the namespace does not need to be prepended when referenced). However the full name, including the namespace, must be used when referenced from static configuration (e.g. command arguments or config file), hence you must prepend the namespace to your traefik-forward-auth middleware reference, as shown in the comments above (e.g. `default-traefik-forward-auth` if your middleware is named `traefik-forward-auth` and is defined in the `default` namespace).
|
||||||
|
|
||||||
|
#### Selective Ingress Authentication in Kubernetes
|
||||||
|
|
||||||
|
If you choose not to enable forward authentication for a specific entrypoint, you can apply the middleware to selected ingressroutes:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apiVersion: traefik.containo.us/v1alpha1
|
||||||
|
kind: IngressRoute
|
||||||
|
metadata:
|
||||||
|
name: whoami
|
||||||
|
labels:
|
||||||
|
app: whoami
|
||||||
|
spec:
|
||||||
|
entryPoints:
|
||||||
|
- http
|
||||||
|
routes:
|
||||||
|
- match: Host(`whoami.example.com`)
|
||||||
|
kind: Rule
|
||||||
|
services:
|
||||||
|
- name: whoami
|
||||||
|
port: 80
|
||||||
|
middlewares:
|
||||||
|
- name: traefik-forward-auth
|
||||||
|
```
|
||||||
|
|
||||||
|
See the examples directory for more examples.
|
||||||
|
|
||||||
|
#### Selective Container Authentication in Swarm
|
||||||
|
|
||||||
|
You can apply labels to selected containers:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
whoami:
|
||||||
|
image: containous/whoami
|
||||||
|
labels:
|
||||||
|
- "traefik.http.routers.whoami.rule=Host(`whoami.example.com`)"
|
||||||
|
- "traefik.http.routers.whoami.middlewares=traefik-forward-auth"
|
||||||
|
```
|
||||||
|
|
||||||
|
See the examples directory for more examples.
|
||||||
|
|
||||||
|
#### Rules Based Authentication
|
||||||
|
|
||||||
|
You can also leverage the `rules` config to selectively apply authentication via traefik-forward-auth. For example if you enabled global authentication by enabling forward authentication for an entire entrypoint, you can still exclude some patterns from requiring authentication:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
# Allow requests to 'dash.example.com'
|
||||||
|
rule.1.action = allow
|
||||||
|
rule.1.rule = Host(`dash.example.com`)
|
||||||
|
|
||||||
|
# Allow requests to `app.example.com/public`
|
||||||
|
rule.two.action = allow
|
||||||
|
rule.two.rule = Host(`app.example.com`) && Path(`/public`)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Operation Modes
|
||||||
|
|
||||||
|
#### Overlay Mode
|
||||||
|
|
||||||
|
Overlay is the default operation mode, in this mode the authorisation endpoint is overlaid onto any domain. By default the `/_oauth` path is used, this can be customised using the `url-path` option.
|
||||||
|
|
||||||
|
The user flow will be:
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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 Mode
|
||||||
|
|
||||||
|
This is an optional mode of operation that is useful when dealing with a large number of subdomains, it is activated by using the `auth-host` config option (see [this example docker-compose.yml](https://github.com/thomseddon/traefik-forward-auth/blob/master/examples/traefik-v2/swarm/docker-compose-auth-host.yml) or [this kubernetes example](https://github.com/thomseddon/traefik-forward-auth/tree/master/examples/traefik-v2/kubernetes/advanced-separate-pod)).
|
||||||
|
|
||||||
|
For example, if you have a few applications: `app1.test.com`, `app2.test.com`, `appN.test.com`, adding every domain to Google's console can become laborious.
|
||||||
|
To utilise an auth host, permit domain level cookies by setting the cookie domain to `test.com` then set the `auth-host` to: `auth.test.com`.
|
||||||
|
|
||||||
|
The user flow will then be:
|
||||||
|
|
||||||
|
1. Request to `app10.test.com/home/page`
|
||||||
|
2. User redirected to Google login
|
||||||
|
3. After Google login, user is redirected to `auth.test.com/_oauth`
|
||||||
|
4. Token, user and CSRF cookie is validated, auth cookie is set to `test.com`
|
||||||
|
5. User is redirected to `app10.test.com/home/page`
|
||||||
|
6. Request is allowed
|
||||||
|
|
||||||
|
With this setup, only `auth.test.com` must be permitted in the Google console.
|
||||||
|
|
||||||
|
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/traefik-v2/swarm/docker-compose-auth-host.yml) example and the [ingressroute resource](https://github.com/thomseddon/traefik-forward-auth/blob/master/examples/traefik-v2/kubernetes/advanced-separate-pod/traefik-forward-auth/ingress.yaml) in a kubernetes example.
|
||||||
|
|
||||||
|
### Logging Out
|
||||||
|
|
||||||
|
The service provides an endpoint to clear a users session and "log them out". The path is created by appending `/logout` to your configured `path` and so with the default settings it will be: `/_oauth/logout`.
|
||||||
|
|
||||||
|
You can use the `logout-redirect` config option to redirect users to another URL following logout (note: the user will not have a valid auth cookie after being logged out).
|
||||||
|
|
||||||
|
Note: This only clears the auth cookie from the users browser and as this service is stateless, it does not invalidate the cookie against future use. So if the cookie was recorded, for example, it could continue to be used for the duration of the cookie lifetime.
|
||||||
|
|
||||||
|
## Copyright
|
||||||
|
|
||||||
|
2018 Thom Seddon
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
[MIT](https://github.com/thomseddon/traefik-forward-auth/blob/master/LICENSE.md)
|
@ -1,6 +1,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
internal "github.com/thomseddon/traefik-forward-auth/internal"
|
internal "github.com/thomseddon/traefik-forward-auth/internal"
|
||||||
@ -24,7 +25,6 @@ func main() {
|
|||||||
http.HandleFunc("/", server.RootHandler)
|
http.HandleFunc("/", server.RootHandler)
|
||||||
|
|
||||||
// Start
|
// Start
|
||||||
log.Debugf("Starting with options: %s", config)
|
log.Infof("Listening on :%d", config.Port)
|
||||||
log.Info("Listening on :4181")
|
log.Info(http.ListenAndServe(fmt.Sprintf(":%d", config.Port), nil))
|
||||||
log.Info(http.ListenAndServe(":4181", nil))
|
|
||||||
}
|
}
|
||||||
|
10
examples-thomseddon/traefik-v1.7/kubernetes/README.md
Normal file
10
examples-thomseddon/traefik-v1.7/kubernetes/README.md
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
|
||||||
|
# Kubernetes
|
||||||
|
|
||||||
|
These examples show how to deploy traefik-forward-auth alongside traefik v1.7.
|
||||||
|
|
||||||
|
The "seperate pod" examples show traefik-forward-auth in it's own pod and leave the deployment of traefik as an exercise for the user (e.g. if using helm).
|
||||||
|
|
||||||
|
The "single pod" examples show traefik and traefik-forward-auth in a single pod.
|
||||||
|
|
||||||
|
Please see the README's in each example for more details.
|
@ -0,0 +1,16 @@
|
|||||||
|
|
||||||
|
# Kubernetes - Advanced Separate Pod Example
|
||||||
|
|
||||||
|
This is an advanced example of how to deploy traefik-forward-auth in it's own pod. This example is a good starting point for those who already have traefik deployed (e.g. using helm).
|
||||||
|
|
||||||
|
This example uses [Selective Authentication](https://github.com/thomseddon/traefik-forward-auth/blob/master/README.md#selective-ingress-authentication-in-kubernetes) to selectively apply forward authentication to each selective ingress, a simple example "whoami" application (deployment, service and ingress) is included for completeness.
|
||||||
|
|
||||||
|
This example leverages kustomise to define Secrets and ConfigMaps, example deployment:
|
||||||
|
|
||||||
|
```
|
||||||
|
# Deploy traefik-forward-auth
|
||||||
|
kubectl apply -k traefik-forward-auth
|
||||||
|
|
||||||
|
# Deploy example whoami app
|
||||||
|
kubectl apply -k whoami
|
||||||
|
```
|
@ -0,0 +1,3 @@
|
|||||||
|
bases:
|
||||||
|
- traefik-forward-auth
|
||||||
|
- whoami
|
@ -0,0 +1,68 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: traefik-forward-auth
|
||||||
|
labels:
|
||||||
|
app: traefik-forward-auth
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: traefik-forward-auth
|
||||||
|
strategy:
|
||||||
|
type: Recreate
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: traefik-forward-auth
|
||||||
|
spec:
|
||||||
|
terminationGracePeriodSeconds: 60
|
||||||
|
containers:
|
||||||
|
- image: thomseddon/traefik-forward-auth:2
|
||||||
|
name: traefik-forward-auth
|
||||||
|
ports:
|
||||||
|
- containerPort: 4181
|
||||||
|
protocol: TCP
|
||||||
|
env:
|
||||||
|
- name: CONFIG
|
||||||
|
value: "/config"
|
||||||
|
- name: DOMAIN
|
||||||
|
value: "example.com"
|
||||||
|
# INSECURE_COOKIE is required unless using https entrypoint
|
||||||
|
- name: INSECURE_COOKIE
|
||||||
|
value: "true"
|
||||||
|
# Remove COOKIE_DOMAIN if not using auth host mode
|
||||||
|
- name: COOKIE_DOMAIN
|
||||||
|
value: "example.com"
|
||||||
|
# Remove AUTH_HOST if not using auth host mode
|
||||||
|
- name: AUTH_HOST
|
||||||
|
value: "auth.example.com"
|
||||||
|
- name: LOG_LEVEL
|
||||||
|
value: "info"
|
||||||
|
- name: PROVIDERS_GOOGLE_CLIENT_ID
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: traefik-forward-auth-secrets
|
||||||
|
key: google-client-id
|
||||||
|
- name: PROVIDERS_GOOGLE_CLIENT_SECRET
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: traefik-forward-auth-secrets
|
||||||
|
key: google-client-secret
|
||||||
|
- name: SECRET
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: traefik-forward-auth-secrets
|
||||||
|
key: secret
|
||||||
|
volumeMounts:
|
||||||
|
- name: configs
|
||||||
|
mountPath: /config
|
||||||
|
subPath: traefik-forward-auth.ini
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
- name: configs
|
||||||
|
configMap:
|
||||||
|
name: configs
|
||||||
|
- name: traefik-forward-auth-secrets
|
||||||
|
secret:
|
||||||
|
secretName: secrets
|
@ -0,0 +1,22 @@
|
|||||||
|
#
|
||||||
|
# NOTE: This is only needed if you are using auth-host mode
|
||||||
|
#
|
||||||
|
apiVersion: extensions/v1beta1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: traefik-forward-auth
|
||||||
|
labels:
|
||||||
|
app: traefik-forward-auth
|
||||||
|
annotations:
|
||||||
|
kubernetes.io/ingress.class: traefik
|
||||||
|
ingress.kubernetes.io/auth-type: forward
|
||||||
|
ingress.kubernetes.io/auth-url: http://traefik-forward-auth:4181
|
||||||
|
ingress.kubernetes.io/auth-response-headers: X-Forwarded-User
|
||||||
|
spec:
|
||||||
|
rules:
|
||||||
|
- host: auth.example.com
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- backend:
|
||||||
|
serviceName: traefik-forward-auth
|
||||||
|
servicePort: auth-http
|
@ -0,0 +1,22 @@
|
|||||||
|
commonLabels:
|
||||||
|
app: traefik-forward-auth
|
||||||
|
|
||||||
|
resources:
|
||||||
|
- deployment.yaml
|
||||||
|
- service.yaml
|
||||||
|
- ingress.yaml # Only needed for auth-host mode
|
||||||
|
|
||||||
|
#
|
||||||
|
# Configs
|
||||||
|
#
|
||||||
|
configMapGenerator:
|
||||||
|
- name: configs
|
||||||
|
files:
|
||||||
|
- traefik-forward-auth.ini
|
||||||
|
|
||||||
|
#
|
||||||
|
# Secrets
|
||||||
|
#
|
||||||
|
secretGenerator:
|
||||||
|
- name: traefik-forward-auth-secrets
|
||||||
|
env: traefik-forward-auth.env
|
@ -0,0 +1,14 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: traefik-forward-auth
|
||||||
|
labels:
|
||||||
|
app: traefik-forward-auth
|
||||||
|
spec:
|
||||||
|
type: ClusterIP
|
||||||
|
selector:
|
||||||
|
app: traefik-forward-auth
|
||||||
|
ports:
|
||||||
|
- name: auth-http
|
||||||
|
port: 4181
|
||||||
|
targetPort: 4181
|
@ -0,0 +1,3 @@
|
|||||||
|
google-client-id=client-id
|
||||||
|
google-client-secret=client-secret
|
||||||
|
secret=something-random
|
@ -0,0 +1,8 @@
|
|||||||
|
rule.example_public.action=allow
|
||||||
|
rule.example_public.rule=Host("stats.example.com") && PathPrefix("/api/public")
|
||||||
|
|
||||||
|
rule.example_api.action=allow
|
||||||
|
rule.example_api.rule=Host("api.example.com") && Headers("X-API-Authorization", "a-long-api-key")
|
||||||
|
|
||||||
|
rule.example_api_query.action=allow
|
||||||
|
rule.example_api_query.rule=Host("api.example.com") && && Query("api_key=a-long-api-key")
|
@ -0,0 +1,19 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: whoami
|
||||||
|
labels:
|
||||||
|
app: whoami
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: whoami
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: whoami
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- image: containous/whoami
|
||||||
|
name: whoami
|
@ -0,0 +1,19 @@
|
|||||||
|
apiVersion: extensions/v1beta1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: whoami
|
||||||
|
labels:
|
||||||
|
app: whoami
|
||||||
|
annotations:
|
||||||
|
kubernetes.io/ingress.class: traefik
|
||||||
|
ingress.kubernetes.io/auth-type: forward
|
||||||
|
ingress.kubernetes.io/auth-url: http://traefik-forward-auth:4181
|
||||||
|
ingress.kubernetes.io/auth-response-headers: X-Forwarded-User
|
||||||
|
spec:
|
||||||
|
rules:
|
||||||
|
- host: whoami.example.com
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- backend:
|
||||||
|
serviceName: whoami
|
||||||
|
servicePort: http
|
@ -0,0 +1,7 @@
|
|||||||
|
commonLabels:
|
||||||
|
app: whoami
|
||||||
|
|
||||||
|
resources:
|
||||||
|
- deployment.yaml
|
||||||
|
- service.yaml
|
||||||
|
- ingress.yaml
|
@ -0,0 +1,14 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: whoami
|
||||||
|
labels:
|
||||||
|
app: whoami
|
||||||
|
spec:
|
||||||
|
type: ClusterIP
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
port: 80
|
||||||
|
selector:
|
||||||
|
app: whoami
|
||||||
|
|
@ -0,0 +1,18 @@
|
|||||||
|
|
||||||
|
# Kubernetes - Advanced Single Pod Example
|
||||||
|
|
||||||
|
This is an advanced example of how to deploy traefik and traefik-forward-auth in a single pod. This example is a good starting point for those who already have a manually defined traefik config (e.g. not using helm).
|
||||||
|
|
||||||
|
This example uses [Global Authentication](https://github.com/thomseddon/traefik-forward-auth/blob/master/README.md#global-authentication) to apply authentication for the entire `https` entrypoint.
|
||||||
|
|
||||||
|
This example also includes SSL via traefik acme/lesencrypt, auth host mode, exposes the traefik dashboard and leverages kustomise. No special config if required for your applications, but a simple example "whoami" application (deployment, service and ingress) is included for completeness.
|
||||||
|
|
||||||
|
Example deployment:
|
||||||
|
|
||||||
|
```
|
||||||
|
# Deploy traefik+traefik-forward-auth
|
||||||
|
kubectl apply -k traefik
|
||||||
|
|
||||||
|
# Deploy whoami app
|
||||||
|
kubectl apply -k whoami
|
||||||
|
```
|
@ -0,0 +1,3 @@
|
|||||||
|
bases:
|
||||||
|
- traefik
|
||||||
|
- whoami
|
@ -0,0 +1,8 @@
|
|||||||
|
rule.example_public.action=allow
|
||||||
|
rule.example_public.rule=Host("stats.example.com") && PathPrefix("/api/public")
|
||||||
|
|
||||||
|
rule.example_api.action=allow
|
||||||
|
rule.example_api.rule=Host("api.example.com") && Headers("X-API-Authorization", "a-long-api-key")
|
||||||
|
|
||||||
|
rule.example_api_query.action=allow
|
||||||
|
rule.example_api_query.rule=Host("api.example.com") && && Query("api_key=a-long-api-key")
|
@ -0,0 +1,169 @@
|
|||||||
|
################################################################
|
||||||
|
# Global configuration
|
||||||
|
################################################################
|
||||||
|
|
||||||
|
# Enable debug mode
|
||||||
|
#
|
||||||
|
# Optional
|
||||||
|
# Default: false
|
||||||
|
#
|
||||||
|
# debug = true
|
||||||
|
|
||||||
|
# Log level
|
||||||
|
#
|
||||||
|
# Optional
|
||||||
|
# Default: "ERROR"
|
||||||
|
#
|
||||||
|
logLevel = "INFO"
|
||||||
|
|
||||||
|
# Entrypoints to be used by frontends that do not specify any entrypoint.
|
||||||
|
# Each frontend can specify its own entrypoints.
|
||||||
|
#
|
||||||
|
# Optional
|
||||||
|
# Default: ["http"]
|
||||||
|
#
|
||||||
|
defaultEntryPoints = ["http", "https"]
|
||||||
|
|
||||||
|
# If set to true invalid SSL certificates are accepted for backends.
|
||||||
|
# This disables detection of man-in-the-middle attacks so should only be used on secure backend networks.
|
||||||
|
#
|
||||||
|
# Optional
|
||||||
|
# Default: false
|
||||||
|
#
|
||||||
|
insecureSkipVerify = true
|
||||||
|
|
||||||
|
################################################################
|
||||||
|
# Entrypoints configuration
|
||||||
|
################################################################
|
||||||
|
|
||||||
|
# Entrypoints definition
|
||||||
|
#
|
||||||
|
# Optional
|
||||||
|
# Default:
|
||||||
|
[entryPoints]
|
||||||
|
[entryPoints.http]
|
||||||
|
address = ":80"
|
||||||
|
compress = true
|
||||||
|
|
||||||
|
[entryPoints.http.redirect]
|
||||||
|
entryPoint = "https"
|
||||||
|
|
||||||
|
[entryPoints.https]
|
||||||
|
address = ":443"
|
||||||
|
compress = true
|
||||||
|
|
||||||
|
[entryPoints.https.tls]
|
||||||
|
|
||||||
|
[entryPoints.https.auth.forward]
|
||||||
|
address = "http://127.0.0.1:4181"
|
||||||
|
authResponseHeaders = ["X-Forwarded-User"]
|
||||||
|
|
||||||
|
[entryPoints.traefik]
|
||||||
|
address = ":8080"
|
||||||
|
|
||||||
|
################################################################
|
||||||
|
# Traefik logs configuration
|
||||||
|
################################################################
|
||||||
|
|
||||||
|
# Traefik logs
|
||||||
|
# Enabled by default and log to stdout
|
||||||
|
#
|
||||||
|
# Optional
|
||||||
|
#
|
||||||
|
[traefikLog]
|
||||||
|
format = "json"
|
||||||
|
|
||||||
|
# Sets the filepath for the traefik log. If not specified, stdout will be used.
|
||||||
|
# Intermediate directories are created if necessary.
|
||||||
|
#
|
||||||
|
# Optional
|
||||||
|
# Default: os.Stdout
|
||||||
|
#
|
||||||
|
# filePath = "log/traefik.log"
|
||||||
|
|
||||||
|
# Format is either "json" or "common".
|
||||||
|
#
|
||||||
|
# Optional
|
||||||
|
# Default: "common"
|
||||||
|
#
|
||||||
|
# format = "common"
|
||||||
|
|
||||||
|
################################################################
|
||||||
|
# Access logs configuration
|
||||||
|
################################################################
|
||||||
|
|
||||||
|
# Enable access logs
|
||||||
|
# By default it will write to stdout and produce logs in the textual
|
||||||
|
# Common Log Format (CLF), extended with additional fields.
|
||||||
|
#
|
||||||
|
# Optional
|
||||||
|
#
|
||||||
|
# [accessLog]
|
||||||
|
|
||||||
|
# Sets the file path for the access log. If not specified, stdout will be used.
|
||||||
|
# Intermediate directories are created if necessary.
|
||||||
|
#
|
||||||
|
# Optional
|
||||||
|
# Default: os.Stdout
|
||||||
|
#
|
||||||
|
# filePath = "/path/to/log/log.txt"
|
||||||
|
|
||||||
|
# Format is either "json" or "common".
|
||||||
|
#
|
||||||
|
# Optional
|
||||||
|
# Default: "common"
|
||||||
|
#
|
||||||
|
# format = "common"
|
||||||
|
|
||||||
|
################################################################
|
||||||
|
# API and dashboard configuration
|
||||||
|
################################################################
|
||||||
|
|
||||||
|
# Enable API and dashboard
|
||||||
|
[api]
|
||||||
|
|
||||||
|
# Name of the related entry point
|
||||||
|
#
|
||||||
|
# Optional
|
||||||
|
# Default: "traefik"
|
||||||
|
#
|
||||||
|
# entryPoint = "traefik"
|
||||||
|
|
||||||
|
# Enabled Dashboard
|
||||||
|
#
|
||||||
|
# Optional
|
||||||
|
# Default: true
|
||||||
|
#
|
||||||
|
# dashboard = false
|
||||||
|
|
||||||
|
################################################################
|
||||||
|
# Ping configuration
|
||||||
|
################################################################
|
||||||
|
|
||||||
|
# Enable ping
|
||||||
|
[ping]
|
||||||
|
|
||||||
|
# Name of the related entry point
|
||||||
|
#
|
||||||
|
# Optional
|
||||||
|
# Default: "traefik"
|
||||||
|
#
|
||||||
|
# entryPoint = "traefik"
|
||||||
|
|
||||||
|
################################################################
|
||||||
|
# Docker configuration backend
|
||||||
|
################################################################
|
||||||
|
|
||||||
|
# Enable Kubernetes configuration backend
|
||||||
|
[kubernetes]
|
||||||
|
|
||||||
|
[acme]
|
||||||
|
KeyType = "RSA4096"
|
||||||
|
email = "you@example.com"
|
||||||
|
storage = "/acme/acme.json"
|
||||||
|
entryPoint = "https"
|
||||||
|
onHostRule = true
|
||||||
|
acmeLogging = true
|
||||||
|
|
||||||
|
[acme.httpChallenge]
|
||||||
|
entryPoint = "http"
|
@ -0,0 +1,94 @@
|
|||||||
|
#
|
||||||
|
# Traefik + Traefik Forward Auth Deployment
|
||||||
|
#
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: traefik
|
||||||
|
labels:
|
||||||
|
app: traefik
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: traefik
|
||||||
|
strategy:
|
||||||
|
type: Recreate
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: traefik
|
||||||
|
spec:
|
||||||
|
serviceAccountName: traefik
|
||||||
|
terminationGracePeriodSeconds: 60
|
||||||
|
containers:
|
||||||
|
- image: traefik:1.7.12
|
||||||
|
name: traefik
|
||||||
|
args:
|
||||||
|
- --configfile=/config/traefik.toml
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
containerPort: 80
|
||||||
|
protocol: TCP
|
||||||
|
- name: https
|
||||||
|
containerPort: 443
|
||||||
|
protocol: TCP
|
||||||
|
- name: dash
|
||||||
|
containerPort: 8080
|
||||||
|
protocol: TCP
|
||||||
|
volumeMounts:
|
||||||
|
- mountPath: /config
|
||||||
|
name: configs
|
||||||
|
- mountPath: /acme
|
||||||
|
name: acme
|
||||||
|
|
||||||
|
- image: thomseddon/traefik-forward-auth:2
|
||||||
|
name: traefik-forward-auth
|
||||||
|
ports:
|
||||||
|
- containerPort: 4181
|
||||||
|
protocol: TCP
|
||||||
|
env:
|
||||||
|
- name: CONFIG
|
||||||
|
value: "/config"
|
||||||
|
- name: DOMAIN
|
||||||
|
value: "example.com"
|
||||||
|
# INSECURE_COOKIE is required if not using a https entrypoint
|
||||||
|
# - name: INSECURE_COOKIE
|
||||||
|
# value: "true"
|
||||||
|
# Remove COOKIE_DOMAIN if not using auth host mode
|
||||||
|
- name: COOKIE_DOMAIN
|
||||||
|
value: "example.com"
|
||||||
|
- name: AUTH_HOST
|
||||||
|
value: "auth.example.com"
|
||||||
|
- name: LOG_LEVEL
|
||||||
|
value: "info"
|
||||||
|
- name: PROVIDERS_GOOGLE_CLIENT_ID
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: traefik-forward-auth-secrets
|
||||||
|
key: google-client-id
|
||||||
|
- name: PROVIDERS_GOOGLE_CLIENT_SECRET
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: traefik-forward-auth-secrets
|
||||||
|
key: google-client-secret
|
||||||
|
- name: SECRET
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: traefik-forward-auth-secrets
|
||||||
|
key: secret
|
||||||
|
volumeMounts:
|
||||||
|
- name: configs
|
||||||
|
mountPath: /config
|
||||||
|
subPath: traefik-forward-auth.ini
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
- name: configs
|
||||||
|
configMap:
|
||||||
|
name: configs
|
||||||
|
- name: traefik-forward-auth-secrets
|
||||||
|
secret:
|
||||||
|
secretName: secrets
|
||||||
|
- name: acme
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: traefik-acme
|
@ -0,0 +1,36 @@
|
|||||||
|
#
|
||||||
|
# Auth Ingress
|
||||||
|
#
|
||||||
|
apiVersion: extensions/v1beta1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: traefik-forward-auth
|
||||||
|
labels:
|
||||||
|
app: traefik
|
||||||
|
spec:
|
||||||
|
rules:
|
||||||
|
- host: auth.example.com
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- backend:
|
||||||
|
serviceName: traefik-forward-auth
|
||||||
|
servicePort: auth-http
|
||||||
|
|
||||||
|
---
|
||||||
|
#
|
||||||
|
# Dash Ingress
|
||||||
|
#
|
||||||
|
apiVersion: extensions/v1beta1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: traefik-dashboard
|
||||||
|
labels:
|
||||||
|
app: traefik
|
||||||
|
spec:
|
||||||
|
rules:
|
||||||
|
- host: traefik.example.com
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- backend:
|
||||||
|
serviceName: traefik-dashboard
|
||||||
|
servicePort: dashboard-http
|
@ -0,0 +1,28 @@
|
|||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
namespace: default
|
||||||
|
commonLabels:
|
||||||
|
app: traefik
|
||||||
|
|
||||||
|
resources:
|
||||||
|
- deployment.yaml
|
||||||
|
- service.yaml
|
||||||
|
- ingress.yaml
|
||||||
|
- pvc.yaml
|
||||||
|
- rbac.yaml
|
||||||
|
|
||||||
|
#
|
||||||
|
# Configs
|
||||||
|
#
|
||||||
|
configMapGenerator:
|
||||||
|
- name: configs
|
||||||
|
files:
|
||||||
|
- configs/traefik.toml
|
||||||
|
- configs/traefik-forward-auth.ini
|
||||||
|
|
||||||
|
#
|
||||||
|
# Secrets
|
||||||
|
#
|
||||||
|
secretGenerator:
|
||||||
|
- name: traefik-forward-auth-secrets
|
||||||
|
env: secrets/traefik-forward-auth.env
|
@ -0,0 +1,17 @@
|
|||||||
|
# Source: traefik/templates/acme-pvc.yaml
|
||||||
|
#
|
||||||
|
# PVC
|
||||||
|
#
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
apiVersion: v1
|
||||||
|
metadata:
|
||||||
|
name: traefik-acme
|
||||||
|
labels:
|
||||||
|
app: traefik
|
||||||
|
spec:
|
||||||
|
accessModes:
|
||||||
|
- "ReadWriteOnce"
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: "1Gi"
|
||||||
|
storageClassName: "local-traefik-acme"
|
@ -0,0 +1,52 @@
|
|||||||
|
#
|
||||||
|
# RBAC
|
||||||
|
# Source: traefik/templates/rbac.yaml
|
||||||
|
#
|
||||||
|
kind: ServiceAccount
|
||||||
|
apiVersion: v1
|
||||||
|
metadata:
|
||||||
|
name: traefik
|
||||||
|
---
|
||||||
|
kind: ClusterRole
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
metadata:
|
||||||
|
name: traefik
|
||||||
|
rules:
|
||||||
|
- apiGroups:
|
||||||
|
- ""
|
||||||
|
resources:
|
||||||
|
- pods
|
||||||
|
- services
|
||||||
|
- endpoints
|
||||||
|
- secrets
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- watch
|
||||||
|
- apiGroups:
|
||||||
|
- extensions
|
||||||
|
resources:
|
||||||
|
- ingresses
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- watch
|
||||||
|
- apiGroups:
|
||||||
|
- extensions
|
||||||
|
resources:
|
||||||
|
- ingresses/status
|
||||||
|
verbs:
|
||||||
|
- update
|
||||||
|
---
|
||||||
|
kind: ClusterRoleBinding
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
metadata:
|
||||||
|
name: traefik
|
||||||
|
roleRef:
|
||||||
|
apiGroup: rbac.authorization.k8s.io
|
||||||
|
kind: ClusterRole
|
||||||
|
name: traefik
|
||||||
|
subjects:
|
||||||
|
- kind: ServiceAccount
|
||||||
|
name: traefik
|
||||||
|
namespace: kube-system
|
@ -0,0 +1,3 @@
|
|||||||
|
google-client-id=client-id
|
||||||
|
google-client-secret=client-secret
|
||||||
|
secret=something-random
|
@ -0,0 +1,58 @@
|
|||||||
|
#
|
||||||
|
# Traefik Service
|
||||||
|
#
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: traefik
|
||||||
|
labels:
|
||||||
|
app: traefik
|
||||||
|
spec:
|
||||||
|
# Use NodePort if required
|
||||||
|
type: LoadBalancer
|
||||||
|
selector:
|
||||||
|
app: traefik
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
port: 80
|
||||||
|
targetPort: 80
|
||||||
|
- name: https
|
||||||
|
port: 443
|
||||||
|
targetPort: 443
|
||||||
|
---
|
||||||
|
#
|
||||||
|
# Auth Service
|
||||||
|
#
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: traefik-forward-auth
|
||||||
|
labels:
|
||||||
|
app: traefik
|
||||||
|
spec:
|
||||||
|
type: ClusterIP
|
||||||
|
selector:
|
||||||
|
app: traefik
|
||||||
|
ports:
|
||||||
|
- name: auth-http
|
||||||
|
port: 4181
|
||||||
|
targetPort: 4181
|
||||||
|
|
||||||
|
---
|
||||||
|
#
|
||||||
|
# Dash Service
|
||||||
|
#
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: traefik-dashboard
|
||||||
|
labels:
|
||||||
|
app: traefik
|
||||||
|
spec:
|
||||||
|
type: ClusterIP
|
||||||
|
selector:
|
||||||
|
app: traefik
|
||||||
|
ports:
|
||||||
|
- name: dashboard-http
|
||||||
|
port: 8080
|
||||||
|
targetPort: 8080
|
@ -0,0 +1,19 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: whoami
|
||||||
|
labels:
|
||||||
|
app: whoami
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: whoami
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: whoami
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- image: containous/whoami
|
||||||
|
name: whoami
|
@ -0,0 +1,16 @@
|
|||||||
|
apiVersion: extensions/v1beta1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: whoami
|
||||||
|
labels:
|
||||||
|
app: whoami
|
||||||
|
annotations:
|
||||||
|
kubernetes.io/ingress.class: traefik
|
||||||
|
spec:
|
||||||
|
rules:
|
||||||
|
- host: whoami.example.com
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- backend:
|
||||||
|
serviceName: whoami
|
||||||
|
servicePort: http
|
@ -0,0 +1,7 @@
|
|||||||
|
commonLabels:
|
||||||
|
app: whoami
|
||||||
|
|
||||||
|
resources:
|
||||||
|
- deployment.yaml
|
||||||
|
- service.yaml
|
||||||
|
- ingress.yaml
|
@ -0,0 +1,14 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: whoami
|
||||||
|
labels:
|
||||||
|
app: whoami
|
||||||
|
spec:
|
||||||
|
type: ClusterIP
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
port: 80
|
||||||
|
selector:
|
||||||
|
app: whoami
|
||||||
|
|
@ -0,0 +1,43 @@
|
|||||||
|
|
||||||
|
# Kubernetes - Simple Separate Pod Example
|
||||||
|
|
||||||
|
This is a simple example of how to deploy traefik-forward-auth in it's own pod with minimal configuration. This example is a good starting point for those who already have traefik deployed (e.g. using helm).
|
||||||
|
|
||||||
|
This example uses annotations to apply authentication to selected ingresses (see `k8s-app.yml`). This means ingresses will not be protected by default, only those with these annotations will require forward authentication. For example:
|
||||||
|
|
||||||
|
```
|
||||||
|
#
|
||||||
|
# Ingress
|
||||||
|
#
|
||||||
|
apiVersion: extensions/v1beta1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: whoami
|
||||||
|
labels:
|
||||||
|
app: whoami
|
||||||
|
annotations:
|
||||||
|
kubernetes.io/ingress.class: traefik
|
||||||
|
ingress.kubernetes.io/auth-type: forward
|
||||||
|
ingress.kubernetes.io/auth-url: http://traefik-forward-auth:4181
|
||||||
|
ingress.kubernetes.io/auth-response-headers: X-Forwarded-User
|
||||||
|
spec:
|
||||||
|
rules:
|
||||||
|
- host: whoami.example.com
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- backend:
|
||||||
|
serviceName: whoami
|
||||||
|
servicePort: http
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Example deployment:
|
||||||
|
```
|
||||||
|
# Deploy traefik-forward-auth
|
||||||
|
kubectl apply -f k8s-traefik-forward-auth.yml
|
||||||
|
|
||||||
|
# Deploy example whoami app
|
||||||
|
kubectl apply -f k8s-app.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
Please see the advanced examples for more details.
|
@ -0,0 +1,62 @@
|
|||||||
|
#
|
||||||
|
# Example Application Deployment
|
||||||
|
#
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: whoami
|
||||||
|
labels:
|
||||||
|
app: whoami
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: whoami
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: whoami
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: whoami
|
||||||
|
image: containous/whoami
|
||||||
|
---
|
||||||
|
#
|
||||||
|
# Service
|
||||||
|
#
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: whoami
|
||||||
|
labels:
|
||||||
|
app: whoami
|
||||||
|
spec:
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
port: 80
|
||||||
|
selector:
|
||||||
|
app: whoami
|
||||||
|
|
||||||
|
---
|
||||||
|
#
|
||||||
|
# Ingress
|
||||||
|
#
|
||||||
|
apiVersion: extensions/v1beta1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: whoami
|
||||||
|
labels:
|
||||||
|
app: whoami
|
||||||
|
annotations:
|
||||||
|
kubernetes.io/ingress.class: traefik
|
||||||
|
ingress.kubernetes.io/auth-type: forward
|
||||||
|
ingress.kubernetes.io/auth-url: http://traefik-forward-auth:4181
|
||||||
|
ingress.kubernetes.io/auth-response-headers: X-Forwarded-User
|
||||||
|
spec:
|
||||||
|
rules:
|
||||||
|
- host: whoami.example.com
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- backend:
|
||||||
|
serviceName: whoami
|
||||||
|
servicePort: http
|
@ -0,0 +1,90 @@
|
|||||||
|
#
|
||||||
|
# Traefik Forward Auth Deployment
|
||||||
|
#
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: traefik-forward-auth
|
||||||
|
labels:
|
||||||
|
app: traefik-forward-auth
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: traefik-forward-auth
|
||||||
|
strategy:
|
||||||
|
type: Recreate
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: traefik-forward-auth
|
||||||
|
spec:
|
||||||
|
terminationGracePeriodSeconds: 60
|
||||||
|
containers:
|
||||||
|
- image: thomseddon/traefik-forward-auth:2
|
||||||
|
name: traefik-forward-auth
|
||||||
|
ports:
|
||||||
|
- containerPort: 4181
|
||||||
|
protocol: TCP
|
||||||
|
env:
|
||||||
|
- name: DOMAIN
|
||||||
|
value: "example.com"
|
||||||
|
# INSECURE_COOKIE is required unless using https entrypoint
|
||||||
|
- name: INSECURE_COOKIE
|
||||||
|
value: "true"
|
||||||
|
- name: PROVIDERS_GOOGLE_CLIENT_ID
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: traefik-forward-auth-secrets
|
||||||
|
key: traefik-forward-auth-google-client-id
|
||||||
|
- name: PROVIDERS_GOOGLE_CLIENT_SECRET
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: traefik-forward-auth-secrets
|
||||||
|
key: traefik-forward-auth-google-client-secret
|
||||||
|
- name: SECRET
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: traefik-forward-auth-secrets
|
||||||
|
key: traefik-forward-auth-secret
|
||||||
|
|
||||||
|
---
|
||||||
|
#
|
||||||
|
# Auth Service
|
||||||
|
#
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: traefik-forward-auth
|
||||||
|
labels:
|
||||||
|
app: traefik-forward-auth
|
||||||
|
spec:
|
||||||
|
type: ClusterIP
|
||||||
|
selector:
|
||||||
|
app: traefik-forward-auth
|
||||||
|
ports:
|
||||||
|
- name: auth-http
|
||||||
|
port: 4181
|
||||||
|
targetPort: 4181
|
||||||
|
|
||||||
|
---
|
||||||
|
#
|
||||||
|
# Secrets
|
||||||
|
#
|
||||||
|
# Kubernetes requires secret values to be converted to base64 when defined
|
||||||
|
# explicitly like this. (use `echo -n 'secret-value' | base64`)
|
||||||
|
#
|
||||||
|
# These are here for completeness, in reality you may define these elsewhere,
|
||||||
|
# for example using kustomize (shown in advanced examples)
|
||||||
|
#
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: traefik-forward-auth-secrets
|
||||||
|
labels:
|
||||||
|
app: traefik-forward-auth
|
||||||
|
type: Opaque
|
||||||
|
data:
|
||||||
|
traefik-forward-auth-google-client-id: base64-client-id
|
||||||
|
traefik-forward-auth-google-client-secret: base64-client-secret
|
||||||
|
traefik-forward-auth-secret: base64-something-random
|
@ -14,7 +14,7 @@ services:
|
|||||||
- /var/run/docker.sock:/var/run/docker.sock
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
|
||||||
whoami1:
|
whoami1:
|
||||||
image: emilevauge/whoami
|
image: containous/whoami
|
||||||
networks:
|
networks:
|
||||||
- traefik
|
- traefik
|
||||||
labels:
|
labels:
|
||||||
@ -23,7 +23,7 @@ services:
|
|||||||
- "traefik.frontend.rule=Host:whoami.yourdomain.com"
|
- "traefik.frontend.rule=Host:whoami.yourdomain.com"
|
||||||
|
|
||||||
traefik-forward-auth:
|
traefik-forward-auth:
|
||||||
image: thomseddon/traefik-forward-auth
|
image: thomseddon/traefik-forward-auth:2
|
||||||
environment:
|
environment:
|
||||||
- PROVIDERS_GOOGLE_CLIENT_ID=your-client-id
|
- PROVIDERS_GOOGLE_CLIENT_ID=your-client-id
|
||||||
- PROVIDERS_GOOGLE_CLIENT_SECRET=your-client-secret
|
- PROVIDERS_GOOGLE_CLIENT_SECRET=your-client-secret
|
||||||
@ -33,7 +33,7 @@ services:
|
|||||||
- AUTH_HOST=auth.yourdomain.com
|
- AUTH_HOST=auth.yourdomain.com
|
||||||
networks:
|
networks:
|
||||||
- traefik
|
- 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:
|
labels:
|
||||||
- traefik.enable=true
|
- traefik.enable=true
|
||||||
- traefik.port=4181
|
- traefik.port=4181
|
@ -2,9 +2,8 @@ version: '3'
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
traefik:
|
traefik:
|
||||||
image: traefik
|
image: traefik:1.7
|
||||||
command: -c /traefik.toml
|
command: -c /traefik.toml
|
||||||
# command: -c /traefik.toml --logLevel=DEBUG
|
|
||||||
ports:
|
ports:
|
||||||
- "8085:80"
|
- "8085:80"
|
||||||
- "8086:8080"
|
- "8086:8080"
|
||||||
@ -15,32 +14,25 @@ services:
|
|||||||
- /var/run/docker.sock:/var/run/docker.sock
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
|
||||||
whoami1:
|
whoami1:
|
||||||
image: emilevauge/whoami
|
image: containous/whoami
|
||||||
networks:
|
networks:
|
||||||
- traefik
|
- traefik
|
||||||
labels:
|
labels:
|
||||||
- "traefik.backend=whoami1"
|
- "traefik.backend=whoami"
|
||||||
- "traefik.enable=true"
|
- "traefik.enable=true"
|
||||||
- "traefik.frontend.rule=Host:whoami.localhost.com"
|
- "traefik.frontend.rule=Host:whoami.localhost.com"
|
||||||
|
|
||||||
whoami2:
|
|
||||||
image: emilevauge/whoami
|
|
||||||
networks:
|
|
||||||
- traefik
|
|
||||||
labels:
|
|
||||||
- "traefik.backend=whoami2"
|
|
||||||
- "traefik.enable=true"
|
|
||||||
- "traefik.frontend.rule=Host:whoami.localhost.org"
|
|
||||||
|
|
||||||
traefik-forward-auth:
|
traefik-forward-auth:
|
||||||
build: ../
|
build: thomseddon/traefik-forward-auth:2
|
||||||
environment:
|
environment:
|
||||||
- PROVIDERS_GOOGLE_CLIENT_ID=your-client-id
|
- DEFAULT_PROVIDER=oidc
|
||||||
- PROVIDERS_GOOGLE_CLIENT_SECRET=your-client-secret
|
- PROVIDERS_OIDC_ISSUER_URL=https://login.microsoftonline.com/{tenant}
|
||||||
|
- PROVIDERS_OIDC_CLIENT_ID=your-client-id
|
||||||
|
- PROVIDERS_OIDC_CLIENT_SECRET=your-client-secret
|
||||||
- SECRET=something-random
|
- SECRET=something-random
|
||||||
- INSECURE_COOKIE=true
|
- INSECURE_COOKIE=true
|
||||||
- COOKIE_DOMAIN=localhost.com
|
- DOMAIN=yourcompany.com
|
||||||
- AUTH_HOST=auth.localhost.com
|
- LOG_LEVEL=debug
|
||||||
networks:
|
networks:
|
||||||
- traefik
|
- traefik
|
||||||
|
|
@ -2,7 +2,7 @@ version: '3'
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
traefik:
|
traefik:
|
||||||
image: traefik
|
image: traefik:1.7
|
||||||
command: -c /traefik.toml --logLevel=DEBUG
|
command: -c /traefik.toml --logLevel=DEBUG
|
||||||
ports:
|
ports:
|
||||||
- "8085:80"
|
- "8085:80"
|
||||||
@ -14,7 +14,7 @@ services:
|
|||||||
- /var/run/docker.sock:/var/run/docker.sock
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
|
||||||
whoami1:
|
whoami1:
|
||||||
image: emilevauge/whoami
|
image: containous/whoami
|
||||||
networks:
|
networks:
|
||||||
- traefik
|
- traefik
|
||||||
labels:
|
labels:
|
||||||
@ -23,13 +23,15 @@ services:
|
|||||||
- "traefik.frontend.rule=Host:whoami.localhost.com"
|
- "traefik.frontend.rule=Host:whoami.localhost.com"
|
||||||
|
|
||||||
traefik-forward-auth:
|
traefik-forward-auth:
|
||||||
image: thomseddon/traefik-forward-auth
|
build: thomseddon/traefik-forward-auth:2
|
||||||
|
command: ./traefik-forward-auth --rule.1.action=allow --rule.1.rule="Path(`/public`)"
|
||||||
environment:
|
environment:
|
||||||
- PROVIDERS_GOOGLE_CLIENT_ID=your-client-id
|
- PROVIDERS_GOOGLE_CLIENT_ID=your-client-id
|
||||||
- PROVIDERS_GOOGLE_CLIENT_SECRET=your-client-secret
|
- PROVIDERS_GOOGLE_CLIENT_SECRET=your-client-secret
|
||||||
- SECRET=something-random
|
- SECRET=something-random
|
||||||
- INSECURE_COOKIE=true
|
- INSECURE_COOKIE=true
|
||||||
- DOMAIN=yourcompany.com
|
- DOMAIN=yourcompany.com
|
||||||
|
- LOG_LEVEL=debug
|
||||||
networks:
|
networks:
|
||||||
- traefik
|
- traefik
|
||||||
|
|
@ -135,3 +135,4 @@
|
|||||||
# Enable Docker configuration backend
|
# Enable Docker configuration backend
|
||||||
[docker]
|
[docker]
|
||||||
exposedByDefault = false
|
exposedByDefault = false
|
||||||
|
network = "traefik"
|
@ -0,0 +1,39 @@
|
|||||||
|
# Kubernetes - Advanced Separate Pod Example
|
||||||
|
|
||||||
|
This is an advanced example of how to deploy traefik-forward-auth in it's own pod. This example is a good starting point for those who already have traefik deployed (e.g. using helm).
|
||||||
|
|
||||||
|
This example uses [Selective Authentication](https://github.com/thomseddon/traefik-forward-auth/blob/master/README.md#selective-ingress-authentication-in-kubernetes) to selectively apply forward authentication to each selective ingresses, for example:
|
||||||
|
|
||||||
|
```
|
||||||
|
apiVersion: traefik.containo.us/v1alpha1
|
||||||
|
kind: IngressRoute
|
||||||
|
metadata:
|
||||||
|
name: whoami
|
||||||
|
labels:
|
||||||
|
app: whoami
|
||||||
|
spec:
|
||||||
|
entryPoints:
|
||||||
|
- https
|
||||||
|
routes:
|
||||||
|
- match: Host(`whoami.example.com`)
|
||||||
|
kind: Rule
|
||||||
|
services:
|
||||||
|
- name: whoami
|
||||||
|
port: 80
|
||||||
|
middlewares:
|
||||||
|
- name: traefik-forward-auth
|
||||||
|
tls:
|
||||||
|
certresolver: default
|
||||||
|
```
|
||||||
|
|
||||||
|
This example also includes SSL via traefik acme/lesencrypt, auth host mode, and leverages kustomise. A simple example "whoami" application (deployment, service and ingress) is included for completeness.
|
||||||
|
|
||||||
|
Example deployment:
|
||||||
|
|
||||||
|
```
|
||||||
|
# Deploy traefik-forward-auth
|
||||||
|
kubectl apply -k traefik-forward-auth
|
||||||
|
|
||||||
|
# Deploy example whoami app
|
||||||
|
kubectl apply -k whoami
|
||||||
|
```
|
@ -0,0 +1,3 @@
|
|||||||
|
bases:
|
||||||
|
- traefik-forward-auth
|
||||||
|
- whoami
|
@ -0,0 +1,8 @@
|
|||||||
|
rule.example_public.action=allow
|
||||||
|
rule.example_public.rule=Host("stats.example.com") && PathPrefix("/api/public")
|
||||||
|
|
||||||
|
rule.example_api.action=allow
|
||||||
|
rule.example_api.rule=Host("api.example.com") && Headers("X-API-Authorization", "a-long-api-key")
|
||||||
|
|
||||||
|
rule.example_api_query.action=allow
|
||||||
|
rule.example_api_query.rule=Host("api.example.com") && && Query("api_key=a-long-api-key")
|
@ -0,0 +1,71 @@
|
|||||||
|
#
|
||||||
|
# Traefik Forward Auth Deployment
|
||||||
|
#
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: traefik-forward-auth
|
||||||
|
labels:
|
||||||
|
app: traefik-forward-auth
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: traefik-forward-auth
|
||||||
|
strategy:
|
||||||
|
type: Recreate
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: traefik-forward-auth
|
||||||
|
spec:
|
||||||
|
serviceAccountName: traefik-ingress-controller
|
||||||
|
terminationGracePeriodSeconds: 60
|
||||||
|
containers:
|
||||||
|
- image: thomseddon/traefik-forward-auth:2
|
||||||
|
name: traefik-forward-auth
|
||||||
|
ports:
|
||||||
|
- containerPort: 4181
|
||||||
|
protocol: TCP
|
||||||
|
env:
|
||||||
|
- name: CONFIG
|
||||||
|
value: "/config"
|
||||||
|
- name: DOMAIN
|
||||||
|
value: "example.com"
|
||||||
|
# INSECURE_COOKIE is required if not using a https entrypoint
|
||||||
|
# - name: INSECURE_COOKIE
|
||||||
|
# value: "true"
|
||||||
|
# Remove COOKIE_DOMAIN if not using auth host mode
|
||||||
|
- name: COOKIE_DOMAIN
|
||||||
|
value: "example.com"
|
||||||
|
- name: AUTH_HOST
|
||||||
|
value: "auth.example.com"
|
||||||
|
- name: LOG_LEVEL
|
||||||
|
value: "info"
|
||||||
|
- name: PROVIDERS_GOOGLE_CLIENT_ID
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: traefik-forward-auth-secrets
|
||||||
|
key: google-client-id
|
||||||
|
- name: PROVIDERS_GOOGLE_CLIENT_SECRET
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: traefik-forward-auth-secrets
|
||||||
|
key: google-client-secret
|
||||||
|
- name: SECRET
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: traefik-forward-auth-secrets
|
||||||
|
key: secret
|
||||||
|
volumeMounts:
|
||||||
|
- name: configs
|
||||||
|
mountPath: /config
|
||||||
|
subPath: traefik-forward-auth.ini
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
- name: configs
|
||||||
|
configMap:
|
||||||
|
name: configs
|
||||||
|
- name: traefik-forward-auth-secrets
|
||||||
|
secret:
|
||||||
|
secretName: traefik-forward-auth-secrets
|
@ -0,0 +1,20 @@
|
|||||||
|
#
|
||||||
|
# Auth Ingress
|
||||||
|
#
|
||||||
|
apiVersion: traefik.containo.us/v1alpha1
|
||||||
|
kind: IngressRoute
|
||||||
|
metadata:
|
||||||
|
name: traefik-forward-auth
|
||||||
|
labels:
|
||||||
|
app: traefik
|
||||||
|
spec:
|
||||||
|
entryPoints:
|
||||||
|
- https
|
||||||
|
routes:
|
||||||
|
- match: Host(`auth.example.com`)
|
||||||
|
kind: Rule
|
||||||
|
services:
|
||||||
|
- name: traefik-forward-auth
|
||||||
|
port: 4181
|
||||||
|
tls:
|
||||||
|
certresolver: default
|
@ -0,0 +1,26 @@
|
|||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
namespace: default
|
||||||
|
commonLabels:
|
||||||
|
app: traefik-forward-auth
|
||||||
|
|
||||||
|
resources:
|
||||||
|
- deployment.yaml
|
||||||
|
- service.yaml
|
||||||
|
- ingress.yaml
|
||||||
|
- middleware.yaml
|
||||||
|
|
||||||
|
#
|
||||||
|
# Configs
|
||||||
|
#
|
||||||
|
configMapGenerator:
|
||||||
|
- name: configs
|
||||||
|
files:
|
||||||
|
- configs/traefik-forward-auth.ini
|
||||||
|
|
||||||
|
#
|
||||||
|
# Secrets
|
||||||
|
#
|
||||||
|
secretGenerator:
|
||||||
|
- name: traefik-forward-auth-secrets
|
||||||
|
env: secrets/traefik-forward-auth.env
|
@ -0,0 +1,9 @@
|
|||||||
|
apiVersion: traefik.containo.us/v1alpha1
|
||||||
|
kind: Middleware
|
||||||
|
metadata:
|
||||||
|
name: traefik-forward-auth
|
||||||
|
spec:
|
||||||
|
forwardAuth:
|
||||||
|
address: http://traefik-forward-auth:4181
|
||||||
|
authResponseHeaders:
|
||||||
|
- X-Forwarded-User
|
@ -0,0 +1,3 @@
|
|||||||
|
google-client-id=client-id
|
||||||
|
google-client-secret=client-secret
|
||||||
|
secret=something-random
|
@ -0,0 +1,17 @@
|
|||||||
|
#
|
||||||
|
# Auth Service
|
||||||
|
#
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: traefik-forward-auth
|
||||||
|
labels:
|
||||||
|
app: traefik
|
||||||
|
spec:
|
||||||
|
type: ClusterIP
|
||||||
|
selector:
|
||||||
|
app: traefik
|
||||||
|
ports:
|
||||||
|
- name: auth-http
|
||||||
|
port: 4181
|
||||||
|
targetPort: 4181
|
@ -0,0 +1,19 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: whoami
|
||||||
|
labels:
|
||||||
|
app: whoami
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: whoami
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: whoami
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- image: containous/whoami
|
||||||
|
name: whoami
|
@ -0,0 +1,19 @@
|
|||||||
|
apiVersion: traefik.containo.us/v1alpha1
|
||||||
|
kind: IngressRoute
|
||||||
|
metadata:
|
||||||
|
name: whoami
|
||||||
|
labels:
|
||||||
|
app: whoami
|
||||||
|
spec:
|
||||||
|
entryPoints:
|
||||||
|
- https
|
||||||
|
routes:
|
||||||
|
- match: Host(`whoami.example.com`)
|
||||||
|
kind: Rule
|
||||||
|
services:
|
||||||
|
- name: whoami
|
||||||
|
port: 80
|
||||||
|
middlewares:
|
||||||
|
- name: traefik-forward-auth
|
||||||
|
tls:
|
||||||
|
certresolver: default
|
@ -0,0 +1,7 @@
|
|||||||
|
commonLabels:
|
||||||
|
app: whoami
|
||||||
|
|
||||||
|
resources:
|
||||||
|
- deployment.yaml
|
||||||
|
- service.yaml
|
||||||
|
- ingress.yaml
|
@ -0,0 +1,14 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: whoami
|
||||||
|
labels:
|
||||||
|
app: whoami
|
||||||
|
spec:
|
||||||
|
type: ClusterIP
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
port: 80
|
||||||
|
selector:
|
||||||
|
app: whoami
|
||||||
|
|
@ -0,0 +1,18 @@
|
|||||||
|
|
||||||
|
# Kubernetes - Advanced Single Pod Example
|
||||||
|
|
||||||
|
This is an advanced example of how to deploy traefik and traefik-forward-auth in a single pod. This example is a good starting point for those who already have a manually defined traefik config (e.g. not using helm).
|
||||||
|
|
||||||
|
This example uses [Global Authentication](https://github.com/thomseddon/traefik-forward-auth/blob/master/README.md#global-authentication) to apply authentication for the entire `https` entrypoint.
|
||||||
|
|
||||||
|
This example also includes SSL via traefik acme/lesencrypt, auth host mode, exposes the traefik dashboard and leverages kustomise. No special config if required for your applications, but a simple example "whoami" application (deployment, service and ingress) is included for completeness.
|
||||||
|
|
||||||
|
Example deployment:
|
||||||
|
|
||||||
|
```
|
||||||
|
# Deploy traefik+traefik-forward-auth
|
||||||
|
kubectl apply -k traefik
|
||||||
|
|
||||||
|
# Deploy whoami app
|
||||||
|
kubectl apply -k whoami
|
||||||
|
```
|
@ -0,0 +1,3 @@
|
|||||||
|
bases:
|
||||||
|
- traefik
|
||||||
|
- whoami
|
@ -0,0 +1,8 @@
|
|||||||
|
rule.example_public.action=allow
|
||||||
|
rule.example_public.rule=Host("stats.example.com") && PathPrefix("/api/public")
|
||||||
|
|
||||||
|
rule.example_api.action=allow
|
||||||
|
rule.example_api.rule=Host("api.example.com") && Headers("X-API-Authorization", "a-long-api-key")
|
||||||
|
|
||||||
|
rule.example_api_query.action=allow
|
||||||
|
rule.example_api_query.rule=Host("api.example.com") && && Query("api_key=a-long-api-key")
|
@ -0,0 +1,103 @@
|
|||||||
|
apiVersion: apiextensions.k8s.io/v1beta1
|
||||||
|
kind: CustomResourceDefinition
|
||||||
|
metadata:
|
||||||
|
name: ingressroutes.traefik.containo.us
|
||||||
|
|
||||||
|
spec:
|
||||||
|
group: traefik.containo.us
|
||||||
|
version: v1alpha1
|
||||||
|
names:
|
||||||
|
kind: IngressRoute
|
||||||
|
plural: ingressroutes
|
||||||
|
singular: ingressroute
|
||||||
|
scope: Namespaced
|
||||||
|
|
||||||
|
---
|
||||||
|
apiVersion: apiextensions.k8s.io/v1beta1
|
||||||
|
kind: CustomResourceDefinition
|
||||||
|
metadata:
|
||||||
|
name: middlewares.traefik.containo.us
|
||||||
|
|
||||||
|
spec:
|
||||||
|
group: traefik.containo.us
|
||||||
|
version: v1alpha1
|
||||||
|
names:
|
||||||
|
kind: Middleware
|
||||||
|
plural: middlewares
|
||||||
|
singular: middleware
|
||||||
|
scope: Namespaced
|
||||||
|
|
||||||
|
---
|
||||||
|
apiVersion: apiextensions.k8s.io/v1beta1
|
||||||
|
kind: CustomResourceDefinition
|
||||||
|
metadata:
|
||||||
|
name: ingressroutetcps.traefik.containo.us
|
||||||
|
|
||||||
|
spec:
|
||||||
|
group: traefik.containo.us
|
||||||
|
version: v1alpha1
|
||||||
|
names:
|
||||||
|
kind: IngressRouteTCP
|
||||||
|
plural: ingressroutetcps
|
||||||
|
singular: ingressroutetcp
|
||||||
|
scope: Namespaced
|
||||||
|
|
||||||
|
---
|
||||||
|
apiVersion: apiextensions.k8s.io/v1beta1
|
||||||
|
kind: CustomResourceDefinition
|
||||||
|
metadata:
|
||||||
|
name: ingressrouteudps.traefik.containo.us
|
||||||
|
|
||||||
|
spec:
|
||||||
|
group: traefik.containo.us
|
||||||
|
version: v1alpha1
|
||||||
|
names:
|
||||||
|
kind: IngressRouteUDP
|
||||||
|
plural: ingressrouteudps
|
||||||
|
singular: ingressrouteudp
|
||||||
|
scope: Namespaced
|
||||||
|
|
||||||
|
---
|
||||||
|
apiVersion: apiextensions.k8s.io/v1beta1
|
||||||
|
kind: CustomResourceDefinition
|
||||||
|
metadata:
|
||||||
|
name: tlsoptions.traefik.containo.us
|
||||||
|
|
||||||
|
spec:
|
||||||
|
group: traefik.containo.us
|
||||||
|
version: v1alpha1
|
||||||
|
names:
|
||||||
|
kind: TLSOption
|
||||||
|
plural: tlsoptions
|
||||||
|
singular: tlsoption
|
||||||
|
scope: Namespaced
|
||||||
|
|
||||||
|
---
|
||||||
|
apiVersion: apiextensions.k8s.io/v1beta1
|
||||||
|
kind: CustomResourceDefinition
|
||||||
|
metadata:
|
||||||
|
name: tlsstores.traefik.containo.us
|
||||||
|
|
||||||
|
spec:
|
||||||
|
group: traefik.containo.us
|
||||||
|
version: v1alpha1
|
||||||
|
names:
|
||||||
|
kind: TLSStore
|
||||||
|
plural: tlsstores
|
||||||
|
singular: tlsstore
|
||||||
|
scope: Namespaced
|
||||||
|
|
||||||
|
---
|
||||||
|
apiVersion: apiextensions.k8s.io/v1beta1
|
||||||
|
kind: CustomResourceDefinition
|
||||||
|
metadata:
|
||||||
|
name: traefikservices.traefik.containo.us
|
||||||
|
|
||||||
|
spec:
|
||||||
|
group: traefik.containo.us
|
||||||
|
version: v1alpha1
|
||||||
|
names:
|
||||||
|
kind: TraefikService
|
||||||
|
plural: traefikservices
|
||||||
|
singular: traefikservice
|
||||||
|
scope: Namespaced
|
@ -0,0 +1,110 @@
|
|||||||
|
#
|
||||||
|
# Traefik + Traefik Forward Auth Deployment
|
||||||
|
#
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: traefik
|
||||||
|
labels:
|
||||||
|
app: traefik
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: traefik
|
||||||
|
strategy:
|
||||||
|
type: Recreate
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: traefik
|
||||||
|
spec:
|
||||||
|
serviceAccountName: traefik-ingress-controller
|
||||||
|
terminationGracePeriodSeconds: 60
|
||||||
|
containers:
|
||||||
|
- image: traefik:2.2
|
||||||
|
name: traefik
|
||||||
|
args:
|
||||||
|
- --api.dashboard
|
||||||
|
- --accesslog
|
||||||
|
- --entryPoints.http.address=:80
|
||||||
|
- --entryPoints.http.http.redirections.entryPoint.to=https
|
||||||
|
- --entryPoints.http.http.redirections.entryPoint.scheme=https
|
||||||
|
- --entryPoints.https.address=:443
|
||||||
|
# We're using "global authentication", so the middleware is defined here on the entrypoint
|
||||||
|
# When a kubernetescrd middleware is applied globally it should take the form <kubernetes-namespace>-<middleware>
|
||||||
|
- --entrypoints.https.http.middlewares=default-traefik-forward-auth
|
||||||
|
- --providers.kubernetescrd
|
||||||
|
- --log.level=info
|
||||||
|
- --log.format=json
|
||||||
|
- --certificatesresolvers.default.acme.email=foo@you.com
|
||||||
|
- --certificatesresolvers.default.acme.storage=/acme/acme.json
|
||||||
|
- --certificatesresolvers.default.acme.storage=/acme/acme.json
|
||||||
|
- --certificatesresolvers.default.acme.httpchallenge.entrypoint=http
|
||||||
|
# Please note that this is the staging Let's Encrypt server.
|
||||||
|
# Once you get things working, you should remove that whole line altogether.
|
||||||
|
- --certificatesresolvers.default.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
containerPort: 80
|
||||||
|
protocol: TCP
|
||||||
|
- name: https
|
||||||
|
containerPort: 443
|
||||||
|
protocol: TCP
|
||||||
|
- name: dash
|
||||||
|
containerPort: 8080
|
||||||
|
protocol: TCP
|
||||||
|
volumeMounts:
|
||||||
|
- mountPath: /acme
|
||||||
|
name: acme
|
||||||
|
|
||||||
|
- image: thomseddon/traefik-forward-auth:2
|
||||||
|
name: traefik-forward-auth
|
||||||
|
ports:
|
||||||
|
- containerPort: 4181
|
||||||
|
protocol: TCP
|
||||||
|
env:
|
||||||
|
- name: CONFIG
|
||||||
|
value: "/config"
|
||||||
|
- name: DOMAIN
|
||||||
|
value: "example.com"
|
||||||
|
# INSECURE_COOKIE is required if not using a https entrypoint
|
||||||
|
# - name: INSECURE_COOKIE
|
||||||
|
# value: "true"
|
||||||
|
# Remove COOKIE_DOMAIN if not using auth host mode
|
||||||
|
- name: COOKIE_DOMAIN
|
||||||
|
value: "example.com"
|
||||||
|
- name: AUTH_HOST
|
||||||
|
value: "auth.example.com"
|
||||||
|
- name: LOG_LEVEL
|
||||||
|
value: "info"
|
||||||
|
- name: PROVIDERS_GOOGLE_CLIENT_ID
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: traefik-forward-auth-secrets
|
||||||
|
key: google-client-id
|
||||||
|
- name: PROVIDERS_GOOGLE_CLIENT_SECRET
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: traefik-forward-auth-secrets
|
||||||
|
key: google-client-secret
|
||||||
|
- name: SECRET
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: traefik-forward-auth-secrets
|
||||||
|
key: secret
|
||||||
|
volumeMounts:
|
||||||
|
- name: configs
|
||||||
|
mountPath: /config
|
||||||
|
subPath: traefik-forward-auth.ini
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
- name: configs
|
||||||
|
configMap:
|
||||||
|
name: configs
|
||||||
|
- name: traefik-forward-auth-secrets
|
||||||
|
secret:
|
||||||
|
secretName: traefik-forward-auth-secrets
|
||||||
|
- name: acme
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: traefik-acme
|
@ -0,0 +1,42 @@
|
|||||||
|
#
|
||||||
|
# Auth Ingress
|
||||||
|
#
|
||||||
|
apiVersion: traefik.containo.us/v1alpha1
|
||||||
|
kind: IngressRoute
|
||||||
|
metadata:
|
||||||
|
name: traefik-forward-auth
|
||||||
|
labels:
|
||||||
|
app: traefik
|
||||||
|
spec:
|
||||||
|
entryPoints:
|
||||||
|
- https
|
||||||
|
routes:
|
||||||
|
- match: Host(`auth.example.com`)
|
||||||
|
kind: Rule
|
||||||
|
services:
|
||||||
|
- name: traefik-forward-auth
|
||||||
|
port: 4181
|
||||||
|
tls:
|
||||||
|
certresolver: default
|
||||||
|
|
||||||
|
---
|
||||||
|
#
|
||||||
|
# Dash Ingress
|
||||||
|
#
|
||||||
|
apiVersion: traefik.containo.us/v1alpha1
|
||||||
|
kind: IngressRoute
|
||||||
|
metadata:
|
||||||
|
name: traefik-dashboard
|
||||||
|
labels:
|
||||||
|
app: traefik
|
||||||
|
spec:
|
||||||
|
entryPoints:
|
||||||
|
- https
|
||||||
|
routes:
|
||||||
|
- match: Host(`traefik.example.com`)
|
||||||
|
kind: Rule
|
||||||
|
services:
|
||||||
|
- name: api@internal
|
||||||
|
kind: TraefikService
|
||||||
|
tls:
|
||||||
|
certresolver: default
|
@ -0,0 +1,29 @@
|
|||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
namespace: default
|
||||||
|
commonLabels:
|
||||||
|
app: traefik
|
||||||
|
|
||||||
|
resources:
|
||||||
|
- crds.yaml
|
||||||
|
- deployment.yaml
|
||||||
|
- service.yaml
|
||||||
|
- ingress.yaml
|
||||||
|
- middleware.yaml
|
||||||
|
- pvc.yaml
|
||||||
|
- rbac.yaml
|
||||||
|
|
||||||
|
#
|
||||||
|
# Configs
|
||||||
|
#
|
||||||
|
configMapGenerator:
|
||||||
|
- name: configs
|
||||||
|
files:
|
||||||
|
- configs/traefik-forward-auth.ini
|
||||||
|
|
||||||
|
#
|
||||||
|
# Secrets
|
||||||
|
#
|
||||||
|
secretGenerator:
|
||||||
|
- name: traefik-forward-auth-secrets
|
||||||
|
env: secrets/traefik-forward-auth.env
|
@ -0,0 +1,9 @@
|
|||||||
|
apiVersion: traefik.containo.us/v1alpha1
|
||||||
|
kind: Middleware
|
||||||
|
metadata:
|
||||||
|
name: traefik-forward-auth
|
||||||
|
spec:
|
||||||
|
forwardAuth:
|
||||||
|
address: http://127.0.0.1:4181
|
||||||
|
authResponseHeaders:
|
||||||
|
- X-Forwarded-User
|
@ -0,0 +1,17 @@
|
|||||||
|
# Source: traefik/templates/acme-pvc.yaml
|
||||||
|
#
|
||||||
|
# PVC
|
||||||
|
#
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
apiVersion: v1
|
||||||
|
metadata:
|
||||||
|
name: traefik-acme
|
||||||
|
labels:
|
||||||
|
app: traefik
|
||||||
|
spec:
|
||||||
|
accessModes:
|
||||||
|
- "ReadWriteOnce"
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: "1Gi"
|
||||||
|
storageClassName: "local-traefik-acme"
|
@ -0,0 +1,66 @@
|
|||||||
|
#
|
||||||
|
# RBAC
|
||||||
|
#
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ServiceAccount
|
||||||
|
metadata:
|
||||||
|
namespace: default
|
||||||
|
name: traefik-ingress-controller
|
||||||
|
---
|
||||||
|
kind: ClusterRole
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||||
|
metadata:
|
||||||
|
name: traefik-ingress-controller
|
||||||
|
rules:
|
||||||
|
- apiGroups:
|
||||||
|
- ""
|
||||||
|
resources:
|
||||||
|
- services
|
||||||
|
- endpoints
|
||||||
|
- secrets
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- watch
|
||||||
|
- apiGroups:
|
||||||
|
- extensions
|
||||||
|
resources:
|
||||||
|
- ingresses
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- watch
|
||||||
|
- apiGroups:
|
||||||
|
- extensions
|
||||||
|
resources:
|
||||||
|
- ingresses/status
|
||||||
|
verbs:
|
||||||
|
- update
|
||||||
|
- apiGroups:
|
||||||
|
- traefik.containo.us
|
||||||
|
resources:
|
||||||
|
- middlewares
|
||||||
|
- ingressroutes
|
||||||
|
- traefikservices
|
||||||
|
- ingressroutetcps
|
||||||
|
- ingressrouteudps
|
||||||
|
- tlsoptions
|
||||||
|
- tlsstores
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- watch
|
||||||
|
|
||||||
|
---
|
||||||
|
kind: ClusterRoleBinding
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||||
|
metadata:
|
||||||
|
name: traefik-ingress-controller
|
||||||
|
roleRef:
|
||||||
|
apiGroup: rbac.authorization.k8s.io
|
||||||
|
kind: ClusterRole
|
||||||
|
name: traefik-ingress-controller
|
||||||
|
subjects:
|
||||||
|
- kind: ServiceAccount
|
||||||
|
name: traefik-ingress-controller
|
||||||
|
namespace: default
|
@ -0,0 +1,3 @@
|
|||||||
|
google-client-id=client-id
|
||||||
|
google-client-secret=client-secret
|
||||||
|
secret=something-random
|
@ -0,0 +1,39 @@
|
|||||||
|
#
|
||||||
|
# Traefik Service
|
||||||
|
#
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: traefik
|
||||||
|
labels:
|
||||||
|
app: traefik
|
||||||
|
spec:
|
||||||
|
# Use NodePort if required
|
||||||
|
type: LoadBalancer
|
||||||
|
selector:
|
||||||
|
app: traefik
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
port: 80
|
||||||
|
targetPort: 80
|
||||||
|
- name: https
|
||||||
|
port: 443
|
||||||
|
targetPort: 443
|
||||||
|
---
|
||||||
|
#
|
||||||
|
# Auth Service
|
||||||
|
#
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: traefik-forward-auth
|
||||||
|
labels:
|
||||||
|
app: traefik
|
||||||
|
spec:
|
||||||
|
type: ClusterIP
|
||||||
|
selector:
|
||||||
|
app: traefik
|
||||||
|
ports:
|
||||||
|
- name: auth-http
|
||||||
|
port: 4181
|
||||||
|
targetPort: 4181
|
@ -0,0 +1,19 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: whoami
|
||||||
|
labels:
|
||||||
|
app: whoami
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: whoami
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: whoami
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- image: containous/whoami
|
||||||
|
name: whoami
|
@ -0,0 +1,17 @@
|
|||||||
|
apiVersion: traefik.containo.us/v1alpha1
|
||||||
|
kind: IngressRoute
|
||||||
|
metadata:
|
||||||
|
name: whoami
|
||||||
|
labels:
|
||||||
|
app: whoami
|
||||||
|
spec:
|
||||||
|
entryPoints:
|
||||||
|
- https
|
||||||
|
routes:
|
||||||
|
- match: Host(`whoami.example.com`)
|
||||||
|
kind: Rule
|
||||||
|
services:
|
||||||
|
- name: whoami
|
||||||
|
port: 80
|
||||||
|
tls:
|
||||||
|
certresolver: default
|
@ -0,0 +1,7 @@
|
|||||||
|
commonLabels:
|
||||||
|
app: whoami
|
||||||
|
|
||||||
|
resources:
|
||||||
|
- deployment.yaml
|
||||||
|
- service.yaml
|
||||||
|
- ingress.yaml
|
@ -0,0 +1,14 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: whoami
|
||||||
|
labels:
|
||||||
|
app: whoami
|
||||||
|
spec:
|
||||||
|
type: ClusterIP
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
port: 80
|
||||||
|
selector:
|
||||||
|
app: whoami
|
||||||
|
|
@ -0,0 +1,39 @@
|
|||||||
|
|
||||||
|
# Kubernetes - Simple Separate Pod Example
|
||||||
|
|
||||||
|
This is a simple example of how to deploy traefik-forward-auth in it's own pod with minimal configuration. This example is a good starting point for those who already have traefik deployed (e.g. using helm).
|
||||||
|
|
||||||
|
This example uses [Selective Authentication](https://github.com/thomseddon/traefik-forward-auth/blob/master/README.md#selective-ingress-authentication-in-kubernetes) to apply forward authentication to selected ingresses. This means ingresses will not be protected by default. Authentication can be applied by adding the `traefik-forward-auth` middleware, for example:
|
||||||
|
|
||||||
|
```
|
||||||
|
apiVersion: traefik.containo.us/v1alpha1
|
||||||
|
kind: IngressRoute
|
||||||
|
metadata:
|
||||||
|
name: whoami
|
||||||
|
labels:
|
||||||
|
app: whoami
|
||||||
|
spec:
|
||||||
|
entryPoints:
|
||||||
|
- http
|
||||||
|
routes:
|
||||||
|
- match: Host(`whoami.example.com`)
|
||||||
|
kind: Rule
|
||||||
|
services:
|
||||||
|
- name: whoami
|
||||||
|
port: 80
|
||||||
|
middlewares:
|
||||||
|
- name: traefik-forward-auth
|
||||||
|
```
|
||||||
|
|
||||||
|
A minimal application example is provided in `k8s-app.yml`.
|
||||||
|
|
||||||
|
Example deployment:
|
||||||
|
```
|
||||||
|
# Deploy traefik-forward-auth
|
||||||
|
kubectl apply -f k8s-traefik-forward-auth.yml
|
||||||
|
|
||||||
|
# Deploy example whoami app
|
||||||
|
kubectl apply -f k8s-app.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
Please see the advanced examples for more details.
|
@ -0,0 +1,60 @@
|
|||||||
|
#
|
||||||
|
# Example Application Deployment
|
||||||
|
#
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: whoami
|
||||||
|
labels:
|
||||||
|
app: whoami
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: whoami
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: whoami
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: whoami
|
||||||
|
image: containous/whoami
|
||||||
|
---
|
||||||
|
#
|
||||||
|
# Service
|
||||||
|
#
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: whoami
|
||||||
|
labels:
|
||||||
|
app: whoami
|
||||||
|
spec:
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
port: 80
|
||||||
|
selector:
|
||||||
|
app: whoami
|
||||||
|
|
||||||
|
---
|
||||||
|
#
|
||||||
|
# IngressRoute
|
||||||
|
#
|
||||||
|
apiVersion: traefik.containo.us/v1alpha1
|
||||||
|
kind: IngressRoute
|
||||||
|
metadata:
|
||||||
|
name: whoami
|
||||||
|
labels:
|
||||||
|
app: whoami
|
||||||
|
spec:
|
||||||
|
entryPoints:
|
||||||
|
- http
|
||||||
|
routes:
|
||||||
|
- match: Host(`whoami.example.com`)
|
||||||
|
kind: Rule
|
||||||
|
services:
|
||||||
|
- name: whoami
|
||||||
|
port: 80
|
||||||
|
middlewares:
|
||||||
|
- name: traefik-forward-auth
|
@ -0,0 +1,104 @@
|
|||||||
|
#
|
||||||
|
# Traefik Forward Auth Deployment
|
||||||
|
#
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: traefik-forward-auth
|
||||||
|
labels:
|
||||||
|
app: traefik-forward-auth
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: traefik-forward-auth
|
||||||
|
strategy:
|
||||||
|
type: Recreate
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: traefik-forward-auth
|
||||||
|
spec:
|
||||||
|
terminationGracePeriodSeconds: 60
|
||||||
|
containers:
|
||||||
|
- image: thomseddon/traefik-forward-auth:2
|
||||||
|
name: traefik-forward-auth
|
||||||
|
ports:
|
||||||
|
- containerPort: 4181
|
||||||
|
protocol: TCP
|
||||||
|
env:
|
||||||
|
- name: DOMAIN
|
||||||
|
value: "example.com"
|
||||||
|
# INSECURE_COOKIE is required unless using https entrypoint
|
||||||
|
- name: INSECURE_COOKIE
|
||||||
|
value: "true"
|
||||||
|
- name: PROVIDERS_GOOGLE_CLIENT_ID
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: traefik-forward-auth-secrets
|
||||||
|
key: traefik-forward-auth-google-client-id
|
||||||
|
- name: PROVIDERS_GOOGLE_CLIENT_SECRET
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: traefik-forward-auth-secrets
|
||||||
|
key: traefik-forward-auth-google-client-secret
|
||||||
|
- name: SECRET
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: traefik-forward-auth-secrets
|
||||||
|
key: traefik-forward-auth-secret
|
||||||
|
|
||||||
|
---
|
||||||
|
#
|
||||||
|
# Auth Service
|
||||||
|
#
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: traefik-forward-auth
|
||||||
|
labels:
|
||||||
|
app: traefik-forward-auth
|
||||||
|
spec:
|
||||||
|
type: ClusterIP
|
||||||
|
selector:
|
||||||
|
app: traefik-forward-auth
|
||||||
|
ports:
|
||||||
|
- name: auth-http
|
||||||
|
port: 4181
|
||||||
|
targetPort: 4181
|
||||||
|
|
||||||
|
---
|
||||||
|
#
|
||||||
|
# Auth Middleware
|
||||||
|
#
|
||||||
|
apiVersion: traefik.containo.us/v1alpha1
|
||||||
|
kind: Middleware
|
||||||
|
metadata:
|
||||||
|
name: traefik-forward-auth
|
||||||
|
spec:
|
||||||
|
forwardAuth:
|
||||||
|
address: http://traefik-forward-auth:4181
|
||||||
|
authResponseHeaders:
|
||||||
|
- X-Forwarded-User
|
||||||
|
|
||||||
|
---
|
||||||
|
#
|
||||||
|
# Secrets
|
||||||
|
#
|
||||||
|
# Kubernetes requires secret values to be converted to base64 when defined
|
||||||
|
# explicitly like this. (use `echo -n 'secret-value' | base64`)
|
||||||
|
#
|
||||||
|
# These are here for completeness, in reality you may define these elsewhere,
|
||||||
|
# for example using kustomize (shown in advanced examples)
|
||||||
|
#
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: traefik-forward-auth-secrets
|
||||||
|
labels:
|
||||||
|
app: traefik-forward-auth
|
||||||
|
type: Opaque
|
||||||
|
data:
|
||||||
|
traefik-forward-auth-google-client-id: base64-client-id
|
||||||
|
traefik-forward-auth-google-client-secret: base64-client-secret
|
||||||
|
traefik-forward-auth-secret: base64-something-random
|
@ -0,0 +1,37 @@
|
|||||||
|
version: '3'
|
||||||
|
|
||||||
|
services:
|
||||||
|
traefik:
|
||||||
|
image: traefik:v2.2
|
||||||
|
command:
|
||||||
|
- --providers.docker
|
||||||
|
# This example uses "global authentication"
|
||||||
|
- --entryPoints.http.address=:80
|
||||||
|
- --entrypoints.http.http.middlewares=traefik-forward-auth
|
||||||
|
ports:
|
||||||
|
- "8085:80"
|
||||||
|
- "8086:8080"
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
|
||||||
|
whoami:
|
||||||
|
image: containous/whoami
|
||||||
|
labels:
|
||||||
|
- "traefik.http.routers.whoami.rule=Host(`whoami.localhost.com`)"
|
||||||
|
|
||||||
|
traefik-forward-auth:
|
||||||
|
image: thomseddon/traefik-forward-auth:2
|
||||||
|
environment:
|
||||||
|
- PROVIDERS_GOOGLE_CLIENT_ID=your-client-id
|
||||||
|
- PROVIDERS_GOOGLE_CLIENT_SECRET=your-client-secret
|
||||||
|
- SECRET=something-random
|
||||||
|
# INSECURE_COOKIE is required if not using a https entrypoint
|
||||||
|
- INSECURE_COOKIE=true
|
||||||
|
- COOKIE_DOMAIN=localhost.com
|
||||||
|
- AUTH_HOST=auth.localhost.com:8085
|
||||||
|
- LOG_LEVEL=debug
|
||||||
|
labels:
|
||||||
|
- "traefik.http.routers.traefik-forward-auth.rule=Host(`auth.localhost.com`)"
|
||||||
|
- "traefik.http.middlewares.traefik-forward-auth.forwardauth.address=http://traefik-forward-auth:4181"
|
||||||
|
- "traefik.http.middlewares.traefik-forward-auth.forwardauth.authResponseHeaders=X-Forwarded-User"
|
||||||
|
- "traefik.http.services.traefik-forward-auth.loadbalancer.server.port=4181"
|
34
examples-thomseddon/traefik-v2/swarm/docker-compose-oidc.yml
Normal file
34
examples-thomseddon/traefik-v2/swarm/docker-compose-oidc.yml
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
version: '3'
|
||||||
|
|
||||||
|
services:
|
||||||
|
traefik:
|
||||||
|
image: traefik:v2.2
|
||||||
|
command: --providers.docker
|
||||||
|
ports:
|
||||||
|
- "8085:80"
|
||||||
|
- "8086:8080"
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
|
||||||
|
whoami:
|
||||||
|
image: containous/whoami
|
||||||
|
labels:
|
||||||
|
- "traefik.http.routers.whoami.rule=Host(`whoami.localhost.com`)"
|
||||||
|
- "traefik.http.routers.whoami.middlewares=traefik-forward-auth"
|
||||||
|
|
||||||
|
traefik-forward-auth:
|
||||||
|
image: thomseddon/traefik-forward-auth:2
|
||||||
|
environment:
|
||||||
|
- DEFAULT_PROVIDER=oidc
|
||||||
|
- PROVIDERS_OIDC_ISSUER_URL=https://login.microsoftonline.com/{tenant}
|
||||||
|
- PROVIDERS_OIDC_CLIENT_ID=your-client-id
|
||||||
|
- PROVIDERS_OIDC_CLIENT_SECRET=your-client-secret
|
||||||
|
- SECRET=something-random
|
||||||
|
# INSECURE_COOKIE is required if not using a https entrypoint
|
||||||
|
- INSECURE_COOKIE=true
|
||||||
|
- LOG_LEVEL=debug
|
||||||
|
labels:
|
||||||
|
- "traefik.http.middlewares.traefik-forward-auth.forwardauth.address=http://traefik-forward-auth:4181"
|
||||||
|
- "traefik.http.middlewares.traefik-forward-auth.forwardauth.authResponseHeaders=X-Forwarded-User"
|
||||||
|
- "traefik.http.services.traefik-forward-auth.loadbalancer.server.port=4181"
|
||||||
|
|
32
examples-thomseddon/traefik-v2/swarm/docker-compose.yml
Normal file
32
examples-thomseddon/traefik-v2/swarm/docker-compose.yml
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
version: '3'
|
||||||
|
|
||||||
|
services:
|
||||||
|
traefik:
|
||||||
|
image: traefik:v2.2
|
||||||
|
command: --providers.docker
|
||||||
|
ports:
|
||||||
|
- "8085:80"
|
||||||
|
- "8086:8080"
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
|
||||||
|
whoami:
|
||||||
|
image: containous/whoami
|
||||||
|
labels:
|
||||||
|
- "traefik.http.routers.whoami.rule=Host(`whoami.localhost.com`)"
|
||||||
|
# This example uses "Selective Authentication"
|
||||||
|
- "traefik.http.routers.whoami.middlewares=traefik-forward-auth"
|
||||||
|
|
||||||
|
traefik-forward-auth:
|
||||||
|
image: thomseddon/traefik-forward-auth:2
|
||||||
|
environment:
|
||||||
|
- PROVIDERS_GOOGLE_CLIENT_ID=your-client-id
|
||||||
|
- PROVIDERS_GOOGLE_CLIENT_SECRET=your-client-secret
|
||||||
|
- SECRET=something-random
|
||||||
|
# INSECURE_COOKIE is required if not using a https entrypoint
|
||||||
|
- INSECURE_COOKIE=true
|
||||||
|
- LOG_LEVEL=debug
|
||||||
|
labels:
|
||||||
|
- "traefik.http.middlewares.traefik-forward-auth.forwardauth.address=http://traefik-forward-auth:4181"
|
||||||
|
- "traefik.http.middlewares.traefik-forward-auth.forwardauth.authResponseHeaders=X-Forwarded-User"
|
||||||
|
- "traefik.http.services.traefik-forward-auth.loadbalancer.server.port=4181"
|
155
examples/auth.yml
Normal file
155
examples/auth.yml
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: traefik-forward-auth
|
||||||
|
data:
|
||||||
|
INSECURE_COOKIE: 'true'
|
||||||
|
COOKIE_DOMAIN: whoami.hottis.de
|
||||||
|
DOMAINS: whoami.hottis.de
|
||||||
|
AUTH_HOST: auth.whoami.hottis.de
|
||||||
|
URL_PATH: /_oauth
|
||||||
|
DEFAULT_PROVIDER: oidc
|
||||||
|
PROVIDERS_OIDC_ISSUER_URL: https://auth2.hottis.de/realms/hottis
|
||||||
|
PROVIDERS_OIDC_CLIENT_ID: whoami
|
||||||
|
REQUIRED_ROLE: whoami_access
|
||||||
|
# ---
|
||||||
|
# apiVersion: v1
|
||||||
|
# kind: Secret
|
||||||
|
# metadata:
|
||||||
|
# name: traefik-forward-auth
|
||||||
|
# type: Opaque
|
||||||
|
# data:
|
||||||
|
# PROVIDERS_OIDC_CLIENT_SECRET: PLACEHOLDER
|
||||||
|
# SECRET: PLACEHOLDER
|
||||||
|
---
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: traefik-forward-auth
|
||||||
|
labels:
|
||||||
|
app: traefik-forward-auth
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: traefik-forward-auth
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: traefik-forward-auth
|
||||||
|
annotations:
|
||||||
|
container.apparmor.security.beta.kubernetes.io/traefik-forward-auth: runtime/default
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: traefik-forward-auth
|
||||||
|
#image: thomseddon/traefik-forward-auth
|
||||||
|
image: wollud1969/traefik-forward-auth:3.0.0
|
||||||
|
imagePullPolicy: Always
|
||||||
|
securityContext:
|
||||||
|
readOnlyRootFilesystem: true
|
||||||
|
runAsNonRoot: true
|
||||||
|
runAsUser: 65534
|
||||||
|
runAsGroup: 65534
|
||||||
|
capabilities:
|
||||||
|
drop:
|
||||||
|
- ALL
|
||||||
|
livenessProbe:
|
||||||
|
failureThreshold: 3
|
||||||
|
tcpSocket:
|
||||||
|
port: 4181
|
||||||
|
initialDelaySeconds: 10
|
||||||
|
periodSeconds: 10
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: '10Mi'
|
||||||
|
cpu: '100m'
|
||||||
|
ports:
|
||||||
|
- containerPort: 4181
|
||||||
|
protocol: TCP
|
||||||
|
env:
|
||||||
|
- name: PROVIDERS_OIDC_CLIENT_SECRET
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: traefik-forward-auth
|
||||||
|
key: PROVIDERS_OIDC_CLIENT_SECRET
|
||||||
|
- name: SECRET
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: traefik-forward-auth
|
||||||
|
key: SECRET
|
||||||
|
- name: LOG_LEVEL
|
||||||
|
value: trace
|
||||||
|
envFrom:
|
||||||
|
- configMapRef:
|
||||||
|
name: traefik-forward-auth
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: traefik-forward-auth
|
||||||
|
labels:
|
||||||
|
app: traefik-forward-auth
|
||||||
|
spec:
|
||||||
|
type: ClusterIP
|
||||||
|
selector:
|
||||||
|
app: traefik-forward-auth
|
||||||
|
ports:
|
||||||
|
- name: auth-http
|
||||||
|
port: 4181
|
||||||
|
targetPort: 4181
|
||||||
|
---
|
||||||
|
apiVersion: cert-manager.io/v1
|
||||||
|
kind: Certificate
|
||||||
|
metadata:
|
||||||
|
name: auth-whoami-hottis-de
|
||||||
|
spec:
|
||||||
|
secretName: auth-whoami-cert
|
||||||
|
duration: 2160h
|
||||||
|
renewBefore: 360h
|
||||||
|
subject:
|
||||||
|
organizations:
|
||||||
|
- hottis-de
|
||||||
|
isCA: false
|
||||||
|
privateKey:
|
||||||
|
algorithm: RSA
|
||||||
|
encoding: PKCS1
|
||||||
|
size: 2048
|
||||||
|
usages:
|
||||||
|
- server auth
|
||||||
|
dnsNames:
|
||||||
|
- auth.whoami.hottis.de
|
||||||
|
issuerRef:
|
||||||
|
name: letsencrypt-production-http
|
||||||
|
kind: ClusterIssuer
|
||||||
|
group: cert-manager.io
|
||||||
|
---
|
||||||
|
apiVersion: traefik.containo.us/v1alpha1
|
||||||
|
kind: IngressRoute
|
||||||
|
metadata:
|
||||||
|
name: traefik-forward-auth
|
||||||
|
labels:
|
||||||
|
app: traefik-forward-auth
|
||||||
|
spec:
|
||||||
|
entryPoints:
|
||||||
|
- websecure
|
||||||
|
routes:
|
||||||
|
- match: Host(`auth.whoami.hottis.de`)
|
||||||
|
kind: Rule
|
||||||
|
services:
|
||||||
|
- name: traefik-forward-auth
|
||||||
|
port: 4181
|
||||||
|
tls:
|
||||||
|
secretName: auth-whoami-cert
|
||||||
|
---
|
||||||
|
apiVersion: traefik.containo.us/v1alpha1
|
||||||
|
kind: Middleware
|
||||||
|
metadata:
|
||||||
|
name: traefik-forward-auth
|
||||||
|
spec:
|
||||||
|
forwardAuth:
|
||||||
|
trustForwardHeader: true
|
||||||
|
address: http://traefik-forward-auth.whoami.svc.cluster.local:4181
|
||||||
|
authResponseHeaders:
|
||||||
|
- X-Forwarded-User
|
||||||
|
|
24
examples/install.sh
Executable file
24
examples/install.sh
Executable file
@ -0,0 +1,24 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
SECRET_CONFIG_DIR=~/Workspace/MyKubernetesEnv/secret-configuration
|
||||||
|
|
||||||
|
NAMESPACE=$(cat namespace)
|
||||||
|
SECRET=$(cat $SECRET_CONFIG_DIR/whoami-secret)
|
||||||
|
PROVIDERS_OIDC_CLIENT_SECRET=$(cat $SECRET_CONFIG_DIR/whoami-oidc-secret)
|
||||||
|
|
||||||
|
kubectl create namespace $NAMESPACE \
|
||||||
|
--dry-run=client \
|
||||||
|
-o yaml | \
|
||||||
|
kubectl -f - apply
|
||||||
|
|
||||||
|
kubectl create secret generic traefik-forward-auth \
|
||||||
|
--dry-run=client \
|
||||||
|
-o yaml \
|
||||||
|
--save-config \
|
||||||
|
--from-literal=PROVIDERS_OIDC_CLIENT_SECRET="$PROVIDERS_OIDC_CLIENT_SECRET" \
|
||||||
|
--from-literal=SECRET="$SECRET" | \
|
||||||
|
kubectl apply -f - -n $NAMESPACE
|
||||||
|
|
||||||
|
kubectl -f auth.yml -n $NAMESPACE apply
|
||||||
|
kubectl -f install.yml -n $NAMESPACE apply
|
||||||
|
|
78
examples/install.yml
Normal file
78
examples/install.yml
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: whoami
|
||||||
|
labels:
|
||||||
|
app: whoami
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: whoami
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: whoami
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- image: containous/whoami
|
||||||
|
name: whoami
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: whoami
|
||||||
|
labels:
|
||||||
|
app: whoami
|
||||||
|
spec:
|
||||||
|
type: ClusterIP
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
port: 80
|
||||||
|
selector:
|
||||||
|
app: whoami
|
||||||
|
---
|
||||||
|
apiVersion: cert-manager.io/v1
|
||||||
|
kind: Certificate
|
||||||
|
metadata:
|
||||||
|
name: whoami-hottis-de
|
||||||
|
spec:
|
||||||
|
secretName: whoami-cert
|
||||||
|
duration: 2160h
|
||||||
|
renewBefore: 360h
|
||||||
|
subject:
|
||||||
|
organizations:
|
||||||
|
- hottis-de
|
||||||
|
isCA: false
|
||||||
|
privateKey:
|
||||||
|
algorithm: RSA
|
||||||
|
encoding: PKCS1
|
||||||
|
size: 2048
|
||||||
|
usages:
|
||||||
|
- server auth
|
||||||
|
dnsNames:
|
||||||
|
- whoami.hottis.de
|
||||||
|
issuerRef:
|
||||||
|
name: letsencrypt-production-http
|
||||||
|
kind: ClusterIssuer
|
||||||
|
group: cert-manager.io
|
||||||
|
---
|
||||||
|
apiVersion: traefik.containo.us/v1alpha1
|
||||||
|
kind: IngressRoute
|
||||||
|
metadata:
|
||||||
|
name: whoami
|
||||||
|
labels:
|
||||||
|
app: whoami
|
||||||
|
spec:
|
||||||
|
entryPoints:
|
||||||
|
- websecure
|
||||||
|
routes:
|
||||||
|
- match: Host(`whoami.hottis.de`)
|
||||||
|
kind: Rule
|
||||||
|
services:
|
||||||
|
- name: whoami
|
||||||
|
port: 80
|
||||||
|
middlewares:
|
||||||
|
- name: traefik-forward-auth
|
||||||
|
tls:
|
||||||
|
secretName: whoami-cert
|
2
examples/namespace
Normal file
2
examples/namespace
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
whoami
|
||||||
|
|
51
go.mod
51
go.mod
@ -1,35 +1,26 @@
|
|||||||
module github.com/thomseddon/traefik-forward-auth
|
module github.com/thomseddon/traefik-forward-auth
|
||||||
|
|
||||||
go 1.12
|
go 1.13
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/VividCortex/gohistogram v1.0.0 // indirect
|
github.com/containous/traefik/v2 v2.1.2
|
||||||
github.com/cenkalti/backoff v2.1.1+incompatible // indirect
|
github.com/coreos/go-oidc v2.1.0+incompatible
|
||||||
github.com/containous/alice v0.0.0-20181107144136-d83ebdd94cbd // indirect
|
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect
|
||||||
github.com/containous/flaeg v1.4.1 // indirect
|
github.com/sirupsen/logrus v1.4.2
|
||||||
github.com/containous/mux v0.0.0-20181024131434-c33f32e26898 // indirect
|
github.com/stretchr/testify v1.4.0
|
||||||
github.com/containous/traefik v2.0.0-alpha2+incompatible
|
github.com/thomseddon/go-flags v1.4.1-0.20190507184247-a3629c504486
|
||||||
github.com/go-acme/lego v2.5.0+incompatible // indirect
|
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45
|
||||||
github.com/go-kit/kit v0.8.0 // indirect
|
gopkg.in/square/go-jose.v2 v2.3.1
|
||||||
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.1-0.20181221193153-c0795c8afcf4
|
// From traefik
|
||||||
github.com/jonboulle/clockwork v0.1.0 // indirect
|
replace (
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
|
github.com/Azure/go-autorest => github.com/Azure/go-autorest v12.4.1+incompatible
|
||||||
github.com/kr/pretty v0.1.0 // indirect
|
github.com/abbot/go-http-auth => github.com/containous/go-http-auth v0.4.1-0.20180112153951-65b0cdae8d7f
|
||||||
github.com/kr/pty v1.1.4 // indirect
|
github.com/docker/docker => github.com/docker/engine v1.4.2-0.20191113042239-ea84732a7725
|
||||||
github.com/miekg/dns v1.1.8 // indirect
|
github.com/go-check/check => github.com/containous/check v0.0.0-20170915194414-ca0bf163426a
|
||||||
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
|
github.com/gorilla/mux => github.com/containous/mux v0.0.0-20181024131434-c33f32e26898
|
||||||
github.com/pkg/errors v0.8.1 // indirect
|
github.com/mailgun/minheap => github.com/containous/minheap v0.0.0-20190809180810-6e71eb837595
|
||||||
github.com/ryanuber/go-glob v1.0.0 // indirect
|
github.com/mailgun/multibuf => github.com/containous/multibuf v0.0.0-20190809014333-8b6c9a7e6bba
|
||||||
github.com/sirupsen/logrus v1.4.1
|
github.com/rancher/go-rancher-metadata => github.com/containous/go-rancher-metadata v0.0.0-20190402144056-c6a65f8b7a28
|
||||||
github.com/stretchr/objx v0.2.0 // indirect
|
|
||||||
github.com/stretchr/testify v1.3.0
|
|
||||||
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
|
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58 // indirect
|
|
||||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894 // indirect
|
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
|
|
||||||
gopkg.in/square/go-jose.v2 v2.3.1 // indirect
|
|
||||||
)
|
)
|
||||||
|
545
go.sum
545
go.sum
@ -1,84 +1,555 @@
|
|||||||
|
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||||
|
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||||
|
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
|
||||||
|
contrib.go.opencensus.io/exporter/ocagent v0.4.12/go.mod h1:450APlNTSR6FrvC3CTRqYosuDstRB9un7SOx2k/9ckA=
|
||||||
|
github.com/Azure/azure-sdk-for-go v32.4.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
|
||||||
|
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
|
||||||
|
github.com/Azure/go-autorest v12.4.1+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
|
||||||
|
github.com/Azure/go-autorest/autorest v0.1.0/go.mod h1:AKyIcETwSUFxIcs/Wnq/C+kwCtlEYGUVd7FPNb2slmg=
|
||||||
|
github.com/Azure/go-autorest/autorest v0.5.0/go.mod h1:9HLKlQjVBH6U3oDfsXOeVc56THsLPw1L03yban4xThw=
|
||||||
|
github.com/Azure/go-autorest/autorest/adal v0.1.0/go.mod h1:MeS4XhScH55IST095THyTxElntu7WqB7pNbZo8Q5G3E=
|
||||||
|
github.com/Azure/go-autorest/autorest/adal v0.2.0/go.mod h1:MeS4XhScH55IST095THyTxElntu7WqB7pNbZo8Q5G3E=
|
||||||
|
github.com/Azure/go-autorest/autorest/azure/auth v0.1.0/go.mod h1:Gf7/i2FUpyb/sGBLIFxTBzrNzBo7aPXXE3ZVeDRwdpM=
|
||||||
|
github.com/Azure/go-autorest/autorest/azure/cli v0.1.0/go.mod h1:Dk8CUAt/b/PzkfeRsWzVG9Yj3ps8mS8ECztu43rdU8U=
|
||||||
|
github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA=
|
||||||
|
github.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0=
|
||||||
|
github.com/Azure/go-autorest/autorest/to v0.2.0/go.mod h1:GunWKJp1AEqgMaGLV+iocmRAJWqST1wQYhyyjXJ3SJc=
|
||||||
|
github.com/Azure/go-autorest/autorest/validation v0.1.0/go.mod h1:Ha3z/SqBeaalWQvokg3NZAlQTalVMtOIAs1aGK7G6u8=
|
||||||
|
github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc=
|
||||||
|
github.com/Azure/go-autorest/tracing v0.1.0/go.mod h1:ROEEAFwXycQw7Sn3DXNtEedEvdeRAgDr0izn4z5Ij88=
|
||||||
|
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||||
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
|
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||||
|
github.com/DataDog/datadog-go v2.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
|
||||||
|
github.com/DataDog/zstd v1.3.6-0.20190409195224-796139022798/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
|
||||||
|
github.com/ExpediaDotCom/haystack-client-go v0.0.0-20190315171017-e7edbdf53a61/go.mod h1:62qWSDaEI0BLykU+zQza5CAKgW0lOy9oBSz3/DvYz4w=
|
||||||
|
github.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
|
||||||
|
github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
|
||||||
|
github.com/Masterminds/sprig v2.20.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o=
|
||||||
|
github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
|
||||||
|
github.com/Microsoft/hcsshim v0.8.6/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg=
|
||||||
|
github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c=
|
||||||
|
github.com/OpenDNS/vegadns2client v0.0.0-20180418235048-a3fa4a771d87/go.mod h1:iGLljf5n9GjT6kc0HBvyI1nOKnGQbNB66VzSNbK5iks=
|
||||||
|
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
|
||||||
|
github.com/Shopify/sarama v1.23.1/go.mod h1:XLH1GYJnLVE0XCr6KdJGVJRTwY30moWNJ4sERjXX6fs=
|
||||||
|
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
|
||||||
github.com/VividCortex/gohistogram v1.0.0 h1:6+hBz+qvs0JOrrNhhmR7lFxo5sINxBCGXrdtl/UvroE=
|
github.com/VividCortex/gohistogram v1.0.0 h1:6+hBz+qvs0JOrrNhhmR7lFxo5sINxBCGXrdtl/UvroE=
|
||||||
github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g=
|
github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g=
|
||||||
github.com/cenkalti/backoff v2.1.1+incompatible h1:tKJnvO2kl0zmb/jA5UKAt4VoEVw1qxKWjE/Bpp46npY=
|
github.com/abronan/valkeyrie v0.0.0-20190822142731-f2e1850dc905/go.mod h1:hTreU6x9m2IP2h8e0TGrSzAXSCI3lxic8/JT5CMknjY=
|
||||||
github.com/cenkalti/backoff v2.1.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
|
github.com/akamai/AkamaiOPEN-edgegrid-golang v0.9.0/go.mod h1:zpDJeKyp9ScW4NNrbdr+Eyxvry3ilGPewKoXw3XGN1k=
|
||||||
|
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||||
|
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||||
|
github.com/aliyun/alibaba-cloud-sdk-go v0.0.0-20190808125512-07798873deee/go.mod h1:myCDvQSzCW+wB1WAlocEru4wMGJxy+vlxHdhegi1CDQ=
|
||||||
|
github.com/aliyun/aliyun-oss-go-sdk v0.0.0-20190307165228-86c17b95fcd5/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8=
|
||||||
|
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
|
||||||
|
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
|
||||||
|
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
|
||||||
|
github.com/armon/go-metrics v0.0.0-20190430140413-ec5e00d3c878/go.mod h1:3AMJUQhVx52RsWOnlkpikZr01T/yAVN2gn0861vByNg=
|
||||||
|
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||||
|
github.com/aws/aws-sdk-go v1.16.23/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
|
||||||
|
github.com/aws/aws-sdk-go v1.23.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
|
||||||
|
github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f/go.mod h1:AuiFmCCPBSrqvVMvuqFuk0qogytodnVFVSN5CeJB8Gc=
|
||||||
|
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||||
|
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||||
|
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
|
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
|
||||||
|
github.com/c0va23/go-proxyprotocol v0.9.1/go.mod h1:TNjUV+llvk8TvWJxlPYAeAYZgSzT/iicNr3nWBWX320=
|
||||||
|
github.com/cenkalti/backoff/v3 v3.0.0 h1:ske+9nBpD9qZsTBoF41nW5L+AIuFBKMeze18XQ3eG1c=
|
||||||
|
github.com/cenkalti/backoff/v3 v3.0.0/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs=
|
||||||
|
github.com/census-instrumentation/opencensus-proto v0.2.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||||
|
github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=
|
||||||
|
github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
|
||||||
|
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||||
|
github.com/cloudflare/cloudflare-go v0.10.2/go.mod h1:qhVI5MKwBGhdNU89ZRz2plgYutcJ5PCekLxXn56w6SY=
|
||||||
|
github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI=
|
||||||
|
github.com/codegangsta/negroni v1.0.0/go.mod h1:v0y3T5G7Y1UlFfyxFn/QLRU4a2EuNau2iZY63YTKWo0=
|
||||||
|
github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=
|
||||||
github.com/containous/alice v0.0.0-20181107144136-d83ebdd94cbd h1:0n+lFLh5zU0l6KSk3KpnDwfbPGAR44aRLgTbCnhRBHU=
|
github.com/containous/alice v0.0.0-20181107144136-d83ebdd94cbd h1:0n+lFLh5zU0l6KSk3KpnDwfbPGAR44aRLgTbCnhRBHU=
|
||||||
github.com/containous/alice v0.0.0-20181107144136-d83ebdd94cbd/go.mod h1:BbQgeDS5i0tNvypwEoF1oNjOJw8knRAE1DnVvjDstcQ=
|
github.com/containous/alice v0.0.0-20181107144136-d83ebdd94cbd/go.mod h1:BbQgeDS5i0tNvypwEoF1oNjOJw8knRAE1DnVvjDstcQ=
|
||||||
github.com/containous/flaeg v1.4.1 h1:VTouP7EF2JeowNvknpP3fJAJLUDsQ1lDHq/QQTQc1xc=
|
github.com/containous/check v0.0.0-20170915194414-ca0bf163426a/go.mod h1:eQOqZ7GoFsLxI7jFKLs7+Nv2Rm1x4FyK8d2NV+yGjwQ=
|
||||||
github.com/containous/flaeg v1.4.1/go.mod h1:wgw6PDtRURXHKFFV6HOqQxWhUc3k3Hmq22jw+n2qDro=
|
github.com/containous/go-http-auth v0.4.1-0.20180112153951-65b0cdae8d7f/go.mod h1:dCmRGidPSLagL8D/2u7yIO6Y/8D/yuYX9EdKrnrhpCA=
|
||||||
|
github.com/containous/go-rancher-metadata v0.0.0-20190402144056-c6a65f8b7a28/go.mod h1:YTAhdMF+tmHPGF7v0uZJ22+XNY/jz1ZYdBCeTZnsrYU=
|
||||||
|
github.com/containous/minheap v0.0.0-20190809180810-6e71eb837595/go.mod h1:+lHFbEasIiQVGzhVDVw/cn0ZaOzde2OwNncp1NhXV4c=
|
||||||
|
github.com/containous/multibuf v0.0.0-20190809014333-8b6c9a7e6bba/go.mod h1:zkWcASFUJEst6QwCrxLdkuw1gvaKqmflEipm+iecV5M=
|
||||||
github.com/containous/mux v0.0.0-20181024131434-c33f32e26898 h1:1srn9voikJGofblBhWy3WuZWqo14Ou7NaswNG/I2yWc=
|
github.com/containous/mux v0.0.0-20181024131434-c33f32e26898 h1:1srn9voikJGofblBhWy3WuZWqo14Ou7NaswNG/I2yWc=
|
||||||
github.com/containous/mux v0.0.0-20181024131434-c33f32e26898/go.mod h1:z8WW7n06n8/1xF9Jl9WmuDeZuHAhfL+bwarNjsciwwg=
|
github.com/containous/mux v0.0.0-20181024131434-c33f32e26898/go.mod h1:z8WW7n06n8/1xF9Jl9WmuDeZuHAhfL+bwarNjsciwwg=
|
||||||
github.com/containous/traefik v2.0.0-alpha2+incompatible h1:5RS6mUAOPQCy1jAmcmxLj2nChIcs3fKuxZxH9AF6ih8=
|
github.com/containous/traefik/v2 v2.1.2 h1:x5lmYFR1LjfBYxiFGKqtvwFmbNtQ91DI1nOTVVEb/bQ=
|
||||||
github.com/containous/traefik v2.0.0-alpha2+incompatible/go.mod h1:epDRqge3JzKOhlSWzOpNYEEKXmM6yfN5tPzDGKk3ljo=
|
github.com/containous/traefik/v2 v2.1.2/go.mod h1:hMgdOHkPB7H/EaBqejJMwo/OZE4PYpWeHBaHVKmOQqY=
|
||||||
|
github.com/coreos/bbolt v1.3.3/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
|
||||||
|
github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||||
|
github.com/coreos/go-oidc v2.1.0+incompatible h1:sdJrfw8akMnCuUlaZU3tE/uYXFgfqom8DBE9so9EBsM=
|
||||||
|
github.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc=
|
||||||
|
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||||
|
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||||
|
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
||||||
|
github.com/cpu/goacmedns v0.0.1/go.mod h1:sesf/pNnCYwUevQEQfEwY0Y3DydlQWSGZbaMElOWxok=
|
||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
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/decker502/dnspod-go v0.2.0/go.mod h1:qsurYu1FgxcDwfSwXJdLt4kRsBLZeosEb9uq4Sy+08g=
|
||||||
github.com/go-acme/lego v2.4.0+incompatible/go.mod h1:yzMNe9CasVUhkquNvti5nAtPmG94USbYxYrZfTkIn0M=
|
github.com/dgrijalva/jwt-go v0.0.0-20160705203006-01aeca54ebda/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||||
github.com/go-acme/lego v2.5.0+incompatible/go.mod h1:yzMNe9CasVUhkquNvti5nAtPmG94USbYxYrZfTkIn0M=
|
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||||
github.com/go-kit/kit v0.8.0 h1:Wz+5lgoB0kkuqLEc6NVmwRknTKP6dTGbSqvhZtBI/j0=
|
github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8=
|
||||||
|
github.com/dnaeon/go-vcr v0.0.0-20180814043457-aafff18a5cc2/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E=
|
||||||
|
github.com/dnsimple/dnsimple-go v0.30.0/go.mod h1:O5TJ0/U6r7AfT8niYNlmohpLbCSG+c71tQlGr9SeGrg=
|
||||||
|
github.com/docker/cli v0.0.0-20190711175710-5b38d82aa076/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||||
|
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||||
|
github.com/docker/docker-credential-helpers v0.6.3/go.mod h1:WRaJzqw3CTB9bk10avuGsjVBZsD05qeibJ1/TYlvc0Y=
|
||||||
|
github.com/docker/engine v1.4.2-0.20191113042239-ea84732a7725/go.mod h1:3CPr2caMgTHxxIAZgEMd3uLYPDlRvPqCpyeRf6ncPcY=
|
||||||
|
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
|
||||||
|
github.com/docker/go-metrics v0.0.0-20181218153428-b84716841b82/go.mod h1:/u0gXw0Gay3ceNrsHubL3BtdOL2fHf93USgMTe0W5dI=
|
||||||
|
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||||
|
github.com/docker/libcompose v0.0.0-20190805081528-eac9fe1b8b03/go.mod h1:EyqDS+Iyca0hS44T7qIMTeO1EOYWWWNOGpufHu9R8cs=
|
||||||
|
github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE=
|
||||||
|
github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM=
|
||||||
|
github.com/donovanhide/eventsource v0.0.0-20170630084216-b8f31a59085e/go.mod h1:56wL82FO0bfMU5RvfXoIwSOP2ggqqxT+tAfNEIyxuHw=
|
||||||
|
github.com/eapache/channels v1.1.0/go.mod h1:jMm2qB5Ubtg9zLd+inMZd2/NUvXgzmWXsDaLyQIGfH0=
|
||||||
|
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
|
||||||
|
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
|
||||||
|
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
|
||||||
|
github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385/go.mod h1:0vRUJqYpeSZifjYj7uP3BG/gKcuzL9xWVV/Y+cK33KM=
|
||||||
|
github.com/elazarl/go-bindata-assetfs v1.0.0/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4=
|
||||||
|
github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
|
||||||
|
github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g=
|
||||||
|
github.com/evanphx/json-patch v0.0.0-20190203023257-5858425f7550/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
|
||||||
|
github.com/evanphx/json-patch v4.5.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
|
||||||
|
github.com/exoscale/egoscale v0.18.1/go.mod h1:Z7OOdzzTOz1Q1PjQXumlz9Wn/CddH0zSYdCF3rnBKXE=
|
||||||
|
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||||
|
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
|
||||||
|
github.com/felixge/httpsnoop v1.0.0/go.mod h1:3+D9sFq0ahK/JeJPhCBUV1xlf4/eIYrUQaxulT0VzX8=
|
||||||
|
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
|
||||||
|
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||||
|
github.com/gambol99/go-marathon v0.0.0-20180614232016-99a156b96fb2/go.mod h1:GLyXJD41gBO/NPKVPGQbhyyC06eugGy15QEZyUkE2/s=
|
||||||
|
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||||
|
github.com/go-acme/lego/v3 v3.2.0 h1:z0zvNlL1niv/1qA06V5X1BRC5PeLoGKAlVaWthXQz9c=
|
||||||
|
github.com/go-acme/lego/v3 v3.2.0/go.mod h1:074uqt+JS6plx+c9Xaiz6+L+GBb+7itGtzfcDM2AhEE=
|
||||||
|
github.com/go-cmd/cmd v1.0.5/go.mod h1:y8q8qlK5wQibcw63djSl/ntiHUHXHGdCkPk0j4QeW4s=
|
||||||
|
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
|
||||||
|
github.com/go-ini/ini v1.44.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
|
||||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||||
|
github.com/go-kit/kit v0.9.0 h1:wDJmvq38kDhkVxi50ni9ykkdUr1PKgqKOoi01fa0Mdk=
|
||||||
|
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||||
|
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||||
|
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||||
|
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||||
|
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||||
|
github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s=
|
||||||
|
github.com/gogo/protobuf v0.0.0-20171007142547-342cbe0a0415/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||||
|
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||||
|
github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||||
|
github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A=
|
||||||
|
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||||
|
github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
|
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
|
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||||
|
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||||
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
|
||||||
|
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||||
|
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||||
|
github.com/google/btree v0.0.0-20160524151835-7d79101e329e/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||||
|
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||||
|
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||||
|
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||||
|
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
|
github.com/google/go-github/v28 v28.0.0/go.mod h1:+5GboIspo7F0NG2qsvfYh7en6F3EK37uyqv+c35AR3s=
|
||||||
|
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
||||||
|
github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI=
|
||||||
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
|
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||||
|
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||||
|
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||||
|
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||||
|
github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY=
|
||||||
|
github.com/googleapis/gnostic v0.1.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY=
|
||||||
|
github.com/gophercloud/gophercloud v0.0.0-20190126172459-c818fa66e4c8/go.mod h1:3WdhXV3rUYy9p6AUW8d94kr+HS62Y4VL9mBnFxsD8q4=
|
||||||
|
github.com/gophercloud/gophercloud v0.3.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8=
|
||||||
|
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||||
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
|
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
|
||||||
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
||||||
github.com/gravitational/trace v0.0.0-20190409171327-f30095ced5ff h1:xL/fJdlTJL6R/6Qk2tPu3EP1NsXgap9hXLvxKH0Ytko=
|
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||||
github.com/gravitational/trace v0.0.0-20190409171327-f30095ced5ff/go.mod h1:RvdOUHE4SHqR3oXlFFKnGzms8a5dugHygGw1bqDstYI=
|
github.com/gravitational/trace v0.0.0-20190726142706-a535a178675f h1:68WxnfBzJRYktZ30fmIjGQ74RsXYLoeH2/NITPktTMY=
|
||||||
github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA=
|
github.com/gravitational/trace v0.0.0-20190726142706-a535a178675f/go.mod h1:RvdOUHE4SHqR3oXlFFKnGzms8a5dugHygGw1bqDstYI=
|
||||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
github.com/gregjones/httpcache v0.0.0-20170728041850-787624de3eb7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
|
||||||
github.com/jessevdk/go-flags v1.4.1-0.20181221193153-c0795c8afcf4 h1:xKkUL6QBojwguhKKetf1SocCAKqc6W7S/mGm9xEGllo=
|
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
|
||||||
github.com/jessevdk/go-flags v1.4.1-0.20181221193153-c0795c8afcf4/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
|
||||||
|
github.com/grpc-ecosystem/grpc-gateway v1.8.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
|
||||||
|
github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
|
||||||
|
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI=
|
||||||
|
github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
|
||||||
|
github.com/hashicorp/consul/api v1.2.0/go.mod h1:1SIkFYi2ZTXUE5Kgt179+4hH33djo11+0Eo2XgTAtkw=
|
||||||
|
github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
|
||||||
|
github.com/hashicorp/consul/sdk v0.2.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
|
||||||
|
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||||
|
github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
|
||||||
|
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
|
||||||
|
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
||||||
|
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
|
||||||
|
github.com/hashicorp/go-msgpack v0.5.5/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
|
||||||
|
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
|
||||||
|
github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
|
||||||
|
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
|
||||||
|
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
|
||||||
|
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
|
||||||
|
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||||
|
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||||
|
github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||||
|
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
|
||||||
|
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||||
|
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||||
|
github.com/hashicorp/golang-lru v0.5.3/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
|
||||||
|
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
|
||||||
|
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
|
||||||
|
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
|
||||||
|
github.com/hashicorp/memberlist v0.1.4/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
|
||||||
|
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
|
||||||
|
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||||
|
github.com/huandu/xstrings v1.2.0/go.mod h1:DvyZB1rfVYsBIigL8HwpZgxHwXozlTgGqn63UyNX5k4=
|
||||||
|
github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df/go.mod h1:QMZY7/J/KSQEhKWFeDesPjMj+wCHReeknARU3wqlyN4=
|
||||||
|
github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
|
||||||
|
github.com/influxdata/influxdb1-client v0.0.0-20190402204710-8ff2fc3824fc/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=
|
||||||
|
github.com/instana/go-sensor v1.4.17-0.20190515112224-78c14625025a/go.mod h1:P1ynE0u78bUBZ2GkWewRpAO1/w1oW9CKDozeueH6QSg=
|
||||||
|
github.com/jcmturner/gofork v0.0.0-20190328161633-dc7c13fece03/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o=
|
||||||
|
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
|
||||||
github.com/jonboulle/clockwork v0.1.0 h1:VKV+ZcuP6l3yW9doeqz6ziZGgcynBVQO+obU0+0hcPo=
|
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/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
|
github.com/json-iterator/go v0.0.0-20180701071628-ab8a2e0c74be/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||||
|
github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||||
|
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||||
|
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||||
|
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||||
|
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||||
|
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||||
|
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||||
|
github.com/kolo/xmlrpc v0.0.0-20190717152603-07c4ee3fd181/go.mod h1:o03bZfuBwAXHetKXuInt4S7omeXUu62/A845kiycsSQ=
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
|
github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/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/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||||
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/labbsr0x/bindman-dns-webhook v1.0.2/go.mod h1:p6b+VCXIR8NYKpDr8/dg1HKfQoRHCdcsROXKvmoehKA=
|
||||||
github.com/miekg/dns v1.1.8/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
github.com/labbsr0x/goh v1.0.1/go.mod h1:8K2UhVoaWXcCU7Lxoa2omWnC8gyW8px7/lmO61c027w=
|
||||||
|
github.com/libkermit/compose v0.0.0-20171122111507-c04e39c026ad/go.mod h1:GyCk/ifDcqsU1tsRMMWqXANnTtxzcwEWscb7j5qmblM=
|
||||||
|
github.com/libkermit/docker v0.0.0-20171122101128-e6674d32b807/go.mod h1:std11u6pTaNwryy0Hy1dTQNdHKka1jNpflEieKtv5VE=
|
||||||
|
github.com/libkermit/docker-check v0.0.0-20171122104347-1113af38e591/go.mod h1:EBQ0jeOrBpOTkquwjmJl4W6z5xqlf5oA2LZfTqRNcO0=
|
||||||
|
github.com/linode/linodego v0.10.0/go.mod h1:cziNP7pbvE3mXIPneHj0oRY8L1WtGEIKlZ8LANE4eXA=
|
||||||
|
github.com/liquidweb/liquidweb-go v1.6.0/go.mod h1:UDcVnAMDkZxpw4Y7NOHkqoeiGacVLEIG/i5J9cyixzQ=
|
||||||
|
github.com/looplab/fsm v0.1.0/go.mod h1:m2VaOfDHxqXBBMgc26m6yUOwkFn8H2AlJDE+jd/uafI=
|
||||||
|
github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ=
|
||||||
|
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||||
|
github.com/mailgun/timetools v0.0.0-20141028012446-7e6055773c51/go.mod h1:RYmqHbhWwIz3z9eVmQ2rx82rulEMG0t+Q1bzfc9DYN4=
|
||||||
|
github.com/mailgun/ttlmap v0.0.0-20170619185759-c1c17f74874f/go.mod h1:8heskWJ5c0v5J9WH89ADhyal1DOZcayll8fSbhB+/9A=
|
||||||
|
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||||
|
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||||
|
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||||
|
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||||
|
github.com/mattn/go-tty v0.0.0-20180219170247-931426f7535a/go.mod h1:XPvLUNfbS4fJH25nqRHfWLMa1ONC8Amw+mIA639KxkE=
|
||||||
|
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||||
|
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||||
|
github.com/miekg/dns v1.1.15 h1:CSSIDtllwGLMoA6zjdKnaE6Tx6eVUxQ29LUgGetiDCI=
|
||||||
|
github.com/miekg/dns v1.1.15/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||||
|
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
|
||||||
|
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
|
||||||
|
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||||
|
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||||
|
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
|
||||||
|
github.com/mitchellh/go-vnc v0.0.0-20150629162542-723ed9867aed/go.mod h1:3rdaFaCv4AyBgu5ALFM0+tSuHrBh6v692nyQe3ikrq0=
|
||||||
|
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
|
||||||
|
github.com/mitchellh/hashstructure v1.0.0/go.mod h1:QjSHrPWS+BGUVBYkbTZWEnOh3G1DutKwClXU/ABz6AQ=
|
||||||
|
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
|
||||||
|
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||||
|
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||||
|
github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||||
|
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||||
|
github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||||
|
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||||
|
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
|
||||||
|
github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04/go.mod h1:5sN+Lt1CaY4wsPvgQH/jsuJi4XO2ssZbdsIizr4CVC8=
|
||||||
|
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms=
|
||||||
|
github.com/nrdcg/auroradns v1.0.0/go.mod h1:6JPXKzIRzZzMqtTDgueIhTi6rFf1QvYE/HzqidhOhjw=
|
||||||
|
github.com/nrdcg/goinwx v0.6.1/go.mod h1:XPiut7enlbEdntAqalBIqcYcTEVhpv/dKWgDCX2SwKQ=
|
||||||
|
github.com/nrdcg/namesilo v0.2.1/go.mod h1:lwMvfQTyYq+BbjJd30ylEG4GPSS6PII0Tia4rRpRiyw=
|
||||||
|
github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
|
||||||
|
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
|
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
|
github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
|
github.com/onsi/gomega v0.0.0-20190113212917-5533ce8a0da3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||||
|
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||||
|
github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||||
|
github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
|
||||||
|
github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
|
||||||
|
github.com/opencontainers/runc v1.0.0-rc8/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
|
||||||
|
github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis=
|
||||||
|
github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74=
|
||||||
|
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
|
||||||
|
github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxSfWAKL3wpBW7V8scJMt8N8gnaMCS9E/cA=
|
||||||
|
github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
|
||||||
|
github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
|
||||||
|
github.com/oracle/oci-go-sdk v7.0.0+incompatible/go.mod h1:VQb79nF8Z2cwLkLS35ukwStZIg5F66tcBccjip/j888=
|
||||||
|
github.com/ovh/go-ovh v0.0.0-20181109152953-ba5adb4cf014/go.mod h1:joRatxRJaZBsY3JAOEMcoOp05CnZzsx4scTxi95DHyQ=
|
||||||
|
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||||
|
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||||
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
|
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
|
||||||
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
|
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
|
||||||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
|
||||||
|
github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
|
||||||
|
github.com/pierrec/lz4 v0.0.0-20190327172049-315a67e90e41/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc=
|
||||||
|
github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc=
|
||||||
|
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
|
||||||
|
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk=
|
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
|
||||||
github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
|
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 h1:J9b7z+QKAmPf4YLrFg6oQUotqHQeUNWwkvo7jZp1GLU=
|
||||||
github.com/sirupsen/logrus v1.4.1 h1:GL2rEmy6nsikmW0r8opw9JIRScdMF5hA8cOYLH7In1k=
|
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA=
|
||||||
|
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||||
|
github.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM=
|
||||||
|
github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
|
||||||
|
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
||||||
|
github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g=
|
||||||
|
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||||
|
github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||||
|
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||||
|
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||||
|
github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
|
||||||
|
github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||||
|
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||||
|
github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc=
|
||||||
|
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||||
|
github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||||
|
github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||||
|
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||||
|
github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
|
||||||
|
github.com/rainycape/memcache v0.0.0-20150622160815-1031fa0ce2f2/go.mod h1:7tZKcyumwBO6qip7RNQ5r77yrssm9bfCowcLEBcU5IA=
|
||||||
|
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20170806203942-52369c62f446/go.mod h1:uYEyJGbgTkfkS4+E/PavXkNJcbFIpEtjt2B0KDQ5+9M=
|
||||||
|
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||||
|
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
|
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||||
|
github.com/sacloud/libsacloud v1.26.1/go.mod h1:79ZwATmHLIFZIMd7sxA3LwzVy/B77uj3LDoToVTxDoQ=
|
||||||
|
github.com/samuel/go-zookeeper v0.0.0-20180130194729-c4fab1ac1bec/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E=
|
||||||
|
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
||||||
|
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
||||||
|
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||||
|
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||||
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
|
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
|
||||||
|
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
|
||||||
|
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||||
|
github.com/skratchdot/open-golang v0.0.0-20160302144031-75fb7ed4208c/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
|
||||||
|
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||||
|
github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||||
|
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
|
||||||
|
github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||||
|
github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
|
||||||
github.com/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/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||||
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
|
github.com/stvp/go-udp-testing v0.0.0-20171104055251-c4434f09ec13/go.mod h1:7jxmlfBCDBXRzr0eAQJ48XC1hBu1np4CS5+cHEYfwpc=
|
||||||
|
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/timewasted/linode v0.0.0-20160829202747-37e84520dcf7/go.mod h1:imsgLplxEC/etjIhdr3dNzV3JeT27LbVu5pYWm0JCBY=
|
||||||
|
github.com/tinylib/msgp v1.0.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
|
||||||
|
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||||
|
github.com/transip/gotransip v0.0.0-20190812104329-6d8d9179b66f/go.mod h1:i0f4R4o2HM0m3DZYQWsj6/MEowD57VzoH0v3d7igeFY=
|
||||||
|
github.com/transip/gotransip v5.8.2+incompatible/go.mod h1:uacMoJVmrfOcscM4Bi5NVg708b7c6rz2oDTWqa7i2Ic=
|
||||||
|
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
|
||||||
|
github.com/uber-go/atomic v1.3.2/go.mod h1:/Ct5t2lcmbJ4OSe/waGBoaVvVqtO0bmtfVNex1PFV8g=
|
||||||
|
github.com/uber/jaeger-client-go v2.21.1+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk=
|
||||||
|
github.com/uber/jaeger-lib v2.2.0+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U=
|
||||||
|
github.com/unrolled/render v1.0.1/go.mod h1:gN9T0NhL4Bfbwu8ann7Ry/TGHYfosul+J0obPf6NBdM=
|
||||||
|
github.com/unrolled/secure v1.0.5/go.mod h1:R6rugAuzh4TQpbFAq69oqZggyBQxFRFQIewtz5z7Jsc=
|
||||||
|
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
||||||
|
github.com/vdemeester/shakers v0.1.0/go.mod h1:IZ1HHynUOQt32iQ3rvAeVddXLd19h/6LWiKsh9RZtAQ=
|
||||||
|
github.com/vulcand/oxy v1.0.0/go.mod h1:6EXgOAl6CRa46/2ZGcDJKf3ywJUp5WtT7vSlGSkvecI=
|
||||||
github.com/vulcand/predicate v1.1.0 h1:Gq/uWopa4rx/tnZu2opOSBqHK63Yqlou/SzrbwdJiNg=
|
github.com/vulcand/predicate v1.1.0 h1:Gq/uWopa4rx/tnZu2opOSBqHK63Yqlou/SzrbwdJiNg=
|
||||||
github.com/vulcand/predicate v1.1.0/go.mod h1:mlccC5IRBoc2cIFmCB8ZM62I3VDb6p2GXESMHa3CnZg=
|
github.com/vulcand/predicate v1.1.0/go.mod h1:mlccC5IRBoc2cIFmCB8ZM62I3VDb6p2GXESMHa3CnZg=
|
||||||
|
github.com/vultr/govultr v0.1.4/go.mod h1:9H008Uxr/C4vFNGLqKx232C206GL0PBHzOP0809bGNA=
|
||||||
|
github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I=
|
||||||
|
github.com/xdg/stringprep v1.0.0/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y=
|
||||||
|
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
|
||||||
|
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
|
||||||
|
github.com/xeipuuv/gojsonschema v1.1.0/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs=
|
||||||
|
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
||||||
|
go.etcd.io/bbolt v1.3.1-etcd.8/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||||
|
go.etcd.io/etcd v3.3.13+incompatible/go.mod h1:yaeTdrJi5lOmYerz05bd8+V7KubZs8YSFZfzsF9A6aI=
|
||||||
|
go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
|
||||||
|
go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
|
||||||
|
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||||
|
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||||
|
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||||
|
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||||
|
go.uber.org/ratelimit v0.0.0-20180316092928-c15da0234277/go.mod h1:2X8KaoNd1J0lZV+PxJk/5+DGbO/tpwLR1m++a7FnB/Y=
|
||||||
|
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||||
|
golang.org/x/crypto v0.0.0-20180621125126-a49355c7e3f8/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
|
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
|
golang.org/x/crypto v0.0.0-20181025213731-e84da0312774/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
|
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
|
golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
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-20190404164418-38d8ce5564a5/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
|
||||||
golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
|
golang.org/x/crypto v0.0.0-20190418165655-df01cb2cc480/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
|
||||||
golang.org/x/crypto v0.0.0-20190422183909-d864b10871cd h1:sMHc2rZHuzQmrbVoSpt9HgerkXPyIeCSO6k0zUMGfFk=
|
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc=
|
||||||
golang.org/x/crypto v0.0.0-20190422183909-d864b10871cd/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ=
|
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
|
golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
|
golang.org/x/exp v0.0.0-20190312203227-4b39c73a6495/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||||
|
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||||
|
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||||
|
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||||
|
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||||
|
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
|
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
|
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
||||||
|
golang.org/x/net v0.0.0-20180611182652-db08ff08e862/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20190206173232-65e2d4e15006/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
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-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190420063019-afa5a82059c6/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA=
|
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20190930134127-c5a3c61f89f3 h1:6KET3Sqa7fkVfD63QnAM81ZeYg5n4HwApOJkufONnHA=
|
||||||
|
golang.org/x/net v0.0.0-20190930134127-c5a3c61f89f3/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
|
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
|
golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
|
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0=
|
||||||
|
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
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-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/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-20180622082034-63fc586f45fe/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/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-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190209173611-3b5209105503/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-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc=
|
|
||||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a h1:aYOabOQFp6Vj6W1F80affTUvO9UxmJRx8K0gsfABByQ=
|
||||||
|
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/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=
|
||||||
|
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
|
golang.org/x/time v0.0.0-20161028155119-f51c12702a4d/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
|
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
|
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
|
golang.org/x/time v0.0.0-20190921001708-c4c64cad1fd0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
|
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||||
|
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
|
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
|
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
|
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
|
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||||
|
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
gonum.org/v1/gonum v0.0.0-20190331200053-3d26580ed485/go.mod h1:2ltnJ7xHfj0zHS40VVPYEAAMTa3ZGguvHGBSJeRWqE0=
|
||||||
|
gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=
|
||||||
|
gonum.org/v1/netlib v0.0.0-20190331212654-76723241ea4e/go.mod h1:kS+toOQn6AQKjmKJ7gzohV1XkqsFehRA2FbsbkopSuQ=
|
||||||
|
google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
|
||||||
|
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||||
|
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||||
|
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||||
|
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||||
|
google.golang.org/appengine v1.5.0 h1:KxkO13IPW4Lslp2bz+KHP2E3gtFlrIGNThxkZQ3g+4c=
|
||||||
|
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||||
|
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||||
|
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||||
|
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||||
|
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||||
|
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
|
||||||
|
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||||
|
google.golang.org/grpc v1.19.1/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||||
|
google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM=
|
||||||
|
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||||
|
google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||||
|
gopkg.in/DataDog/dd-trace-go.v1 v1.16.1/go.mod h1:DVp8HmDh8PuTu2Z0fVVlBsyWaC++fzwVCaGWylTe3tg=
|
||||||
|
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
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=
|
||||||
|
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||||
|
gopkg.in/h2non/gock.v1 v1.0.15/go.mod h1:sX4zAkdYX1TRGJ2JY156cFspQn4yRWn6p9EMdODlynE=
|
||||||
|
gopkg.in/inf.v0 v0.9.0/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||||
|
gopkg.in/ini.v1 v1.42.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||||
|
gopkg.in/ini.v1 v1.44.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||||
|
gopkg.in/jcmturner/aescts.v1 v1.0.1/go.mod h1:nsR8qBOg+OucoIW+WMhB3GspUQXq9XorLnQb9XtvcOo=
|
||||||
|
gopkg.in/jcmturner/dnsutils.v1 v1.0.1/go.mod h1:m3v+5svpVOhtFAP/wSz+yzh4Mc0Fg7eRhxkJMWSIz9Q=
|
||||||
|
gopkg.in/jcmturner/goidentity.v3 v3.0.0/go.mod h1:oG2kH0IvSYNIu80dVAyu/yoefjq1mNfM5bm88whjWx4=
|
||||||
|
gopkg.in/jcmturner/gokrb5.v7 v7.2.3/go.mod h1:l8VISx+WGYp+Fp7KRbsiUuXTTOnxIc3Tuvyavf11/WM=
|
||||||
|
gopkg.in/jcmturner/rpc.v1 v1.1.0/go.mod h1:YIdkC4XfD6GXbzje11McwsDuOlZQSb9W4vfLvuNnlv8=
|
||||||
|
gopkg.in/ns1/ns1-go.v2 v2.0.0-20190730140822-b51389932cbc/go.mod h1:VV+3haRsgDiVLxyifmMBrBIuCWFBPYKbRssXB9z67Hw=
|
||||||
|
gopkg.in/redis.v5 v5.2.9/go.mod h1:6gtv0/+A4iM08kdRfocWYB3bLX2tebpNtfKlFT6H4mY=
|
||||||
|
gopkg.in/resty.v1 v1.9.1/go.mod h1:vo52Hzryw9PnPHcJfPsBiFW62XhNx5OczbV9y+IMpgc=
|
||||||
|
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
|
||||||
gopkg.in/square/go-jose.v2 v2.3.1 h1:SK5KegNXmKmqE342YYN2qPHEnUYeoMiXXl1poUlI+o4=
|
gopkg.in/square/go-jose.v2 v2.3.1 h1:SK5KegNXmKmqE342YYN2qPHEnUYeoMiXXl1poUlI+o4=
|
||||||
gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
|
gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
|
||||||
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||||
|
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
|
||||||
|
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||||
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
|
||||||
|
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
|
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
|
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
|
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
|
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
|
k8s.io/api v0.0.0-20190718183219-b59d8169aab5/go.mod h1:TBhBqb1AWbBQbW3XRusr7n7E4v2+5ZY8r8sAMnyFC5A=
|
||||||
|
k8s.io/apimachinery v0.0.0-20190612205821-1799e75a0719/go.mod h1:I4A+glKBHiTgiEjQiCCQfCAIcIMFGt291SmsvcrFzJA=
|
||||||
|
k8s.io/client-go v0.0.0-20190718183610-8e956561bbf5/go.mod h1:ozblAqkW495yoAX60QZyxQBq5W0YixE9Ffn4F91RO0g=
|
||||||
|
k8s.io/code-generator v0.0.0-20190612205613-18da4a14b22b/go.mod h1:G8bQwmHm2eafm5bgtX67XDZQ8CWKSGu9DekI+yN4Y5I=
|
||||||
|
k8s.io/gengo v0.0.0-20190116091435-f8a0810f38af/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=
|
||||||
|
k8s.io/klog v0.3.1/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
|
||||||
|
k8s.io/kube-openapi v0.0.0-20190228160746-b3a7cee44a30/go.mod h1:BXM9ceUBTj2QnfH2MK1odQs778ajze1RxcmP6S8RVVc=
|
||||||
|
k8s.io/utils v0.0.0-20190221042446-c2654d5206da/go.mod h1:8k8uAuAQ0rXslZKaEWd0c3oVhZz7sSzSiPnVZayjIX0=
|
||||||
|
modernc.org/cc v1.0.0/go.mod h1:1Sk4//wdnYJiUIxnW8ddKpaOJCF37yAdqYnkxUpaYxw=
|
||||||
|
modernc.org/golex v1.0.0/go.mod h1:b/QX9oBD/LhixY6NDh+IdGv17hgB+51fET1i2kPSmvk=
|
||||||
|
modernc.org/mathutil v1.0.0/go.mod h1:wU0vUrJsVWBZ4P6e7xtFJEhFSNsfRLJ8H458uRjg03k=
|
||||||
|
modernc.org/strutil v1.0.0/go.mod h1:lstksw84oURvj9y3tn8lGvRxyRC1S2+g5uuIzNfIOBs=
|
||||||
|
modernc.org/xc v1.0.0/go.mod h1:mRNCo0bvLjGhHO9WsyuKVU4q0ceiDDDoEeWDJHrNx8I=
|
||||||
|
mvdan.cc/xurls/v2 v2.0.0/go.mod h1:2/webFPYOXN9jp/lzuj0zuAVlF+9g4KPFJANH1oJhRU=
|
||||||
|
sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
|
||||||
|
BIN
images/Keycloak-Access-Settings.png
Normal file
BIN
images/Keycloak-Access-Settings.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 50 KiB |
BIN
images/Keycloak-Capability-Config.png
Normal file
BIN
images/Keycloak-Capability-Config.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 42 KiB |
BIN
images/Keycloak-Client-Mapper.png
Normal file
BIN
images/Keycloak-Client-Mapper.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 72 KiB |
BIN
images/Keycloak-Client-Roles.png
Normal file
BIN
images/Keycloak-Client-Roles.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 37 KiB |
BIN
images/Keycloak-General-Settings.png
Normal file
BIN
images/Keycloak-General-Settings.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 25 KiB |
236
internal/auth.go
236
internal/auth.go
@ -17,118 +17,127 @@ import (
|
|||||||
|
|
||||||
// Request Validation
|
// Request Validation
|
||||||
|
|
||||||
|
// ValidateCookie verifies that a cookie matches the expected format of:
|
||||||
// Cookie = hash(secret, cookie domain, email, expires)|expires|email
|
// Cookie = hash(secret, cookie domain, email, expires)|expires|email
|
||||||
func ValidateCookie(r *http.Request, c *http.Cookie) (bool, string, error) {
|
func ValidateCookie(r *http.Request, c *http.Cookie) (string, error) {
|
||||||
parts := strings.Split(c.Value, "|")
|
parts := strings.Split(c.Value, "|")
|
||||||
|
|
||||||
if len(parts) != 3 {
|
if len(parts) != 3 {
|
||||||
return false, "", errors.New("Invalid cookie format")
|
return "", errors.New("Invalid cookie format")
|
||||||
}
|
}
|
||||||
|
|
||||||
mac, err := base64.URLEncoding.DecodeString(parts[0])
|
mac, err := base64.URLEncoding.DecodeString(parts[0])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, "", errors.New("Unable to decode cookie mac")
|
return "", errors.New("Unable to decode cookie mac")
|
||||||
}
|
}
|
||||||
|
|
||||||
expectedSignature := cookieSignature(r, parts[2], parts[1])
|
expectedSignature := cookieSignature(r, parts[2], parts[1])
|
||||||
expected, err := base64.URLEncoding.DecodeString(expectedSignature)
|
expected, err := base64.URLEncoding.DecodeString(expectedSignature)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, "", errors.New("Unable to generate mac")
|
return "", errors.New("Unable to generate mac")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Valid token?
|
// Valid token?
|
||||||
if !hmac.Equal(mac, expected) {
|
if !hmac.Equal(mac, expected) {
|
||||||
return false, "", errors.New("Invalid cookie mac")
|
return "", errors.New("Invalid cookie mac")
|
||||||
}
|
}
|
||||||
|
|
||||||
expires, err := strconv.ParseInt(parts[1], 10, 64)
|
expires, err := strconv.ParseInt(parts[1], 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, "", errors.New("Unable to parse cookie expiry")
|
return "", errors.New("Unable to parse cookie expiry")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Has it expired?
|
// Has it expired?
|
||||||
if time.Unix(expires, 0).Before(time.Now()) {
|
if time.Unix(expires, 0).Before(time.Now()) {
|
||||||
return false, "", errors.New("Cookie has expired")
|
return "", errors.New("Cookie has expired")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Looks valid
|
// Looks valid
|
||||||
return true, parts[2], nil
|
return parts[2], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate email
|
// ValidateEmail checks if the given email address matches either a whitelisted
|
||||||
func ValidateEmail(email string) bool {
|
// email address, as defined by the "whitelist" config parameter. Or is part of
|
||||||
found := false
|
// a permitted domain, as defined by the "domains" config parameter
|
||||||
if len(config.Whitelist) > 0 {
|
func ValidateEmail(email, ruleName string) bool {
|
||||||
for _, whitelist := range config.Whitelist {
|
// Use global config by default
|
||||||
|
whitelist := config.Whitelist
|
||||||
|
domains := config.Domains
|
||||||
|
|
||||||
|
if rule, ok := config.Rules[ruleName]; ok {
|
||||||
|
// Override with rule config if found
|
||||||
|
if len(rule.Whitelist) > 0 || len(rule.Domains) > 0 {
|
||||||
|
whitelist = rule.Whitelist
|
||||||
|
domains = rule.Domains
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do we have any validation to perform?
|
||||||
|
if len(whitelist) == 0 && len(domains) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Email whitelist validation
|
||||||
|
if len(whitelist) > 0 {
|
||||||
|
if ValidateWhitelist(email, whitelist) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we're not matching *either*, stop here
|
||||||
|
if !config.MatchWhitelistOrDomain {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Domain validation
|
||||||
|
if len(domains) > 0 && ValidateDomains(email, domains) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateWhitelist checks if the email is in whitelist
|
||||||
|
func ValidateWhitelist(email string, whitelist CommaSeparatedList) bool {
|
||||||
|
for _, whitelist := range whitelist {
|
||||||
if email == whitelist {
|
if email == whitelist {
|
||||||
found = true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if len(config.Domains) > 0 {
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateDomains checks if the email matches a whitelisted domain
|
||||||
|
func ValidateDomains(email string, domains CommaSeparatedList) bool {
|
||||||
parts := strings.Split(email, "@")
|
parts := strings.Split(email, "@")
|
||||||
if len(parts) < 2 {
|
if len(parts) < 2 {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
for _, domain := range config.Domains {
|
for _, domain := range domains {
|
||||||
if domain == parts[1] {
|
if domain == parts[1] {
|
||||||
found = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return found
|
return false
|
||||||
}
|
|
||||||
|
|
||||||
// OAuth Methods
|
|
||||||
|
|
||||||
// Get login url
|
|
||||||
func GetLoginURL(r *http.Request, nonce string) string {
|
|
||||||
state := fmt.Sprintf("%s:%s", nonce, returnUrl(r))
|
|
||||||
|
|
||||||
// TODO: Support multiple providers
|
|
||||||
return config.Providers.Google.GetLoginURL(redirectUri(r), state)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exchange code for token
|
|
||||||
|
|
||||||
func ExchangeCode(r *http.Request) (string, error) {
|
|
||||||
code := r.URL.Query().Get("code")
|
|
||||||
|
|
||||||
// TODO: Support multiple providers
|
|
||||||
return config.Providers.Google.ExchangeCode(redirectUri(r), code)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get user with token
|
|
||||||
|
|
||||||
func GetUser(token string) (provider.User, error) {
|
|
||||||
// TODO: Support multiple providers
|
|
||||||
return config.Providers.Google.GetUser(token)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Utility methods
|
// Utility methods
|
||||||
|
|
||||||
// Get the redirect base
|
// Get the redirect base
|
||||||
func redirectBase(r *http.Request) string {
|
func redirectBase(r *http.Request) string {
|
||||||
proto := r.Header.Get("X-Forwarded-Proto")
|
return fmt.Sprintf("%s://%s", r.Header.Get("X-Forwarded-Proto"), r.Host)
|
||||||
host := r.Header.Get("X-Forwarded-Host")
|
|
||||||
|
|
||||||
return fmt.Sprintf("%s://%s", proto, host)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// // Return url
|
// Return url
|
||||||
func returnUrl(r *http.Request) string {
|
func returnUrl(r *http.Request) string {
|
||||||
path := r.Header.Get("X-Forwarded-Uri")
|
return fmt.Sprintf("%s%s", redirectBase(r), r.URL.Path)
|
||||||
|
|
||||||
return fmt.Sprintf("%s%s", redirectBase(r), path)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get oauth redirect uri
|
// Get oauth redirect uri
|
||||||
func redirectUri(r *http.Request) string {
|
func redirectUri(r *http.Request) string {
|
||||||
if use, _ := useAuthDomain(r); use {
|
if use, _ := useAuthDomain(r); use {
|
||||||
proto := r.Header.Get("X-Forwarded-Proto")
|
p := r.Header.Get("X-Forwarded-Proto")
|
||||||
return fmt.Sprintf("%s://%s%s", proto, config.AuthHost, config.Path)
|
return fmt.Sprintf("%s://%s%s", p, config.AuthHost, config.Path)
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Sprintf("%s%s", redirectBase(r), config.Path)
|
return fmt.Sprintf("%s%s", redirectBase(r), config.Path)
|
||||||
@ -141,7 +150,7 @@ func useAuthDomain(r *http.Request) (bool, string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Does the request match a given cookie domain?
|
// Does the request match a given cookie domain?
|
||||||
reqMatch, reqHost := matchCookieDomains(r.Header.Get("X-Forwarded-Host"))
|
reqMatch, reqHost := matchCookieDomains(r.Host)
|
||||||
|
|
||||||
// Do any of the auth hosts match a cookie domain?
|
// Do any of the auth hosts match a cookie domain?
|
||||||
authMatch, authHost := matchCookieDomains(config.AuthHost)
|
authMatch, authHost := matchCookieDomains(config.AuthHost)
|
||||||
@ -152,7 +161,7 @@ func useAuthDomain(r *http.Request) (bool, string) {
|
|||||||
|
|
||||||
// Cookie methods
|
// Cookie methods
|
||||||
|
|
||||||
// Create an auth cookie
|
// MakeCookie creates an auth cookie
|
||||||
func MakeCookie(r *http.Request, email string) *http.Cookie {
|
func MakeCookie(r *http.Request, email string) *http.Cookie {
|
||||||
expires := cookieExpiry()
|
expires := cookieExpiry()
|
||||||
mac := cookieSignature(r, email, fmt.Sprintf("%d", expires.Unix()))
|
mac := cookieSignature(r, email, fmt.Sprintf("%d", expires.Unix()))
|
||||||
@ -169,23 +178,44 @@ func MakeCookie(r *http.Request, email string) *http.Cookie {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make a CSRF cookie (used during login only)
|
// ClearCookie clears the auth cookie
|
||||||
|
func ClearCookie(r *http.Request) *http.Cookie {
|
||||||
|
return &http.Cookie{
|
||||||
|
Name: config.CookieName,
|
||||||
|
Value: "",
|
||||||
|
Path: "/",
|
||||||
|
Domain: cookieDomain(r),
|
||||||
|
HttpOnly: true,
|
||||||
|
Secure: !config.InsecureCookie,
|
||||||
|
Expires: time.Now().Local().Add(time.Hour * -1),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildCSRFCookieName(nonce string) string {
|
||||||
|
return config.CSRFCookieName + "_" + nonce[:6]
|
||||||
|
}
|
||||||
|
|
||||||
|
// MakeCSRFCookie makes a csrf cookie (used during login only)
|
||||||
|
//
|
||||||
|
// Note, CSRF cookies live shorter than auth cookies, a fixed 1h.
|
||||||
|
// That's because some CSRF cookies may belong to auth flows that don't complete
|
||||||
|
// and thus may not get cleared by ClearCookie.
|
||||||
func MakeCSRFCookie(r *http.Request, nonce string) *http.Cookie {
|
func MakeCSRFCookie(r *http.Request, nonce string) *http.Cookie {
|
||||||
return &http.Cookie{
|
return &http.Cookie{
|
||||||
Name: config.CSRFCookieName,
|
Name: buildCSRFCookieName(nonce),
|
||||||
Value: nonce,
|
Value: nonce,
|
||||||
Path: "/",
|
Path: "/",
|
||||||
Domain: csrfCookieDomain(r),
|
Domain: csrfCookieDomain(r),
|
||||||
HttpOnly: true,
|
HttpOnly: true,
|
||||||
Secure: !config.InsecureCookie,
|
Secure: !config.InsecureCookie,
|
||||||
Expires: cookieExpiry(),
|
Expires: time.Now().Local().Add(time.Hour * 1),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a cookie to clear csrf cookie
|
// ClearCSRFCookie makes an expired csrf cookie to clear csrf cookie
|
||||||
func ClearCSRFCookie(r *http.Request) *http.Cookie {
|
func ClearCSRFCookie(r *http.Request, c *http.Cookie) *http.Cookie {
|
||||||
return &http.Cookie{
|
return &http.Cookie{
|
||||||
Name: config.CSRFCookieName,
|
Name: c.Name,
|
||||||
Value: "",
|
Value: "",
|
||||||
Path: "/",
|
Path: "/",
|
||||||
Domain: csrfCookieDomain(r),
|
Domain: csrfCookieDomain(r),
|
||||||
@ -195,29 +225,49 @@ func ClearCSRFCookie(r *http.Request) *http.Cookie {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate the csrf cookie against state
|
// FindCSRFCookie extracts the CSRF cookie from the request based on state.
|
||||||
func ValidateCSRFCookie(r *http.Request, c *http.Cookie) (bool, string, error) {
|
func FindCSRFCookie(r *http.Request, state string) (c *http.Cookie, err error) {
|
||||||
state := r.URL.Query().Get("state")
|
// Check for CSRF cookie
|
||||||
|
return r.Cookie(buildCSRFCookieName(state))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateCSRFCookie validates the csrf cookie against state
|
||||||
|
func ValidateCSRFCookie(c *http.Cookie, state string) (valid bool, provider string, redirect string, err error) {
|
||||||
if len(c.Value) != 32 {
|
if len(c.Value) != 32 {
|
||||||
return false, "", errors.New("Invalid CSRF cookie value")
|
return false, "", "", errors.New("Invalid CSRF cookie value")
|
||||||
}
|
|
||||||
|
|
||||||
if len(state) < 34 {
|
|
||||||
return false, "", errors.New("Invalid CSRF state value")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check nonce match
|
// Check nonce match
|
||||||
if c.Value != state[:32] {
|
if c.Value != state[:32] {
|
||||||
return false, "", errors.New("CSRF cookie does not match state")
|
return false, "", "", errors.New("CSRF cookie does not match state")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Valid, return redirect
|
// Extract provider
|
||||||
return true, state[33:], nil
|
params := state[33:]
|
||||||
|
split := strings.Index(params, ":")
|
||||||
|
if split == -1 {
|
||||||
|
return false, "", "", errors.New("Invalid CSRF state format")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Valid, return provider and redirect
|
||||||
|
return true, params[:split], params[split+1:], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MakeState generates a state value
|
||||||
|
func MakeState(r *http.Request, p provider.Provider, nonce string) string {
|
||||||
|
return fmt.Sprintf("%s:%s:%s", nonce, p.Name(), returnUrl(r))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateState checks whether the state is of right length.
|
||||||
|
func ValidateState(state string) error {
|
||||||
|
if len(state) < 34 {
|
||||||
|
return errors.New("Invalid CSRF state value")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nonce generates a random nonce
|
||||||
func Nonce() (error, string) {
|
func Nonce() (error, string) {
|
||||||
// Make nonce
|
|
||||||
nonce := make([]byte, 16)
|
nonce := make([]byte, 16)
|
||||||
_, err := rand.Read(nonce)
|
_, err := rand.Read(nonce)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -229,10 +279,8 @@ func Nonce() (error, string) {
|
|||||||
|
|
||||||
// Cookie domain
|
// Cookie domain
|
||||||
func cookieDomain(r *http.Request) string {
|
func cookieDomain(r *http.Request) string {
|
||||||
host := r.Header.Get("X-Forwarded-Host")
|
|
||||||
|
|
||||||
// Check if any of the given cookie domains matches
|
// Check if any of the given cookie domains matches
|
||||||
_, domain := matchCookieDomains(host)
|
_, domain := matchCookieDomains(r.Host)
|
||||||
return domain
|
return domain
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -242,7 +290,7 @@ func csrfCookieDomain(r *http.Request) string {
|
|||||||
if use, domain := useAuthDomain(r); use {
|
if use, domain := useAuthDomain(r); use {
|
||||||
host = domain
|
host = domain
|
||||||
} else {
|
} else {
|
||||||
host = r.Header.Get("X-Forwarded-Host")
|
host = r.Host
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove port
|
// Remove port
|
||||||
@ -273,21 +321,20 @@ func cookieSignature(r *http.Request, email, expires string) string {
|
|||||||
return base64.URLEncoding.EncodeToString(hash.Sum(nil))
|
return base64.URLEncoding.EncodeToString(hash.Sum(nil))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get cookie expirary
|
// Get cookie expiry
|
||||||
func cookieExpiry() time.Time {
|
func cookieExpiry() time.Time {
|
||||||
return time.Now().Local().Add(config.Lifetime)
|
return time.Now().Local().Add(config.Lifetime)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cookie Domain
|
// CookieDomain holds cookie domain info
|
||||||
|
|
||||||
// Cookie Domain
|
|
||||||
type CookieDomain struct {
|
type CookieDomain struct {
|
||||||
Domain string `description:"TEST1"`
|
Domain string
|
||||||
DomainLen int `description:"TEST2"`
|
DomainLen int
|
||||||
SubDomain string `description:"TEST3"`
|
SubDomain string
|
||||||
SubDomainLen int `description:"TEST4"`
|
SubDomainLen int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewCookieDomain creates a new CookieDomain from the given domain string
|
||||||
func NewCookieDomain(domain string) *CookieDomain {
|
func NewCookieDomain(domain string) *CookieDomain {
|
||||||
return &CookieDomain{
|
return &CookieDomain{
|
||||||
Domain: domain,
|
Domain: domain,
|
||||||
@ -297,6 +344,7 @@ func NewCookieDomain(domain string) *CookieDomain {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Match checks if the given host matches this CookieDomain
|
||||||
func (c *CookieDomain) Match(host string) bool {
|
func (c *CookieDomain) Match(host string) bool {
|
||||||
// Exact domain match?
|
// Exact domain match?
|
||||||
if host == c.Domain {
|
if host == c.Domain {
|
||||||
@ -311,19 +359,22 @@ func (c *CookieDomain) Match(host string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UnmarshalFlag converts a string to a CookieDomain
|
||||||
func (c *CookieDomain) UnmarshalFlag(value string) error {
|
func (c *CookieDomain) UnmarshalFlag(value string) error {
|
||||||
*c = *NewCookieDomain(value)
|
*c = *NewCookieDomain(value)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MarshalFlag converts a CookieDomain to a string
|
||||||
func (c *CookieDomain) MarshalFlag() (string, error) {
|
func (c *CookieDomain) MarshalFlag() (string, error) {
|
||||||
return c.Domain, nil
|
return c.Domain, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Legacy support for comma separated list of cookie domains
|
// CookieDomains provides legacy sypport for comma separated list of cookie domains
|
||||||
|
|
||||||
type CookieDomains []CookieDomain
|
type CookieDomains []CookieDomain
|
||||||
|
|
||||||
|
// UnmarshalFlag converts a comma separated list of cookie domains to an array
|
||||||
|
// of CookieDomains
|
||||||
func (c *CookieDomains) UnmarshalFlag(value string) error {
|
func (c *CookieDomains) UnmarshalFlag(value string) error {
|
||||||
if len(value) > 0 {
|
if len(value) > 0 {
|
||||||
for _, d := range strings.Split(value, ",") {
|
for _, d := range strings.Split(value, ",") {
|
||||||
@ -334,6 +385,7 @@ func (c *CookieDomains) UnmarshalFlag(value string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MarshalFlag converts an array of CookieDomain to a comma seperated list
|
||||||
func (c *CookieDomains) MarshalFlag() (string, error) {
|
func (c *CookieDomains) MarshalFlag() (string, error) {
|
||||||
var domains []string
|
var domains []string
|
||||||
for _, d := range *c {
|
for _, d := range *c {
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
package tfa
|
package tfa
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
@ -24,28 +24,24 @@ func TestAuthValidateCookie(t *testing.T) {
|
|||||||
|
|
||||||
// Should require 3 parts
|
// Should require 3 parts
|
||||||
c.Value = ""
|
c.Value = ""
|
||||||
valid, _, err := ValidateCookie(r, c)
|
_, err := ValidateCookie(r, c)
|
||||||
assert.False(valid)
|
|
||||||
if assert.Error(err) {
|
if assert.Error(err) {
|
||||||
assert.Equal("Invalid cookie format", err.Error())
|
assert.Equal("Invalid cookie format", err.Error())
|
||||||
}
|
}
|
||||||
c.Value = "1|2"
|
c.Value = "1|2"
|
||||||
valid, _, err = ValidateCookie(r, c)
|
_, err = ValidateCookie(r, c)
|
||||||
assert.False(valid)
|
|
||||||
if assert.Error(err) {
|
if assert.Error(err) {
|
||||||
assert.Equal("Invalid cookie format", err.Error())
|
assert.Equal("Invalid cookie format", err.Error())
|
||||||
}
|
}
|
||||||
c.Value = "1|2|3|4"
|
c.Value = "1|2|3|4"
|
||||||
valid, _, err = ValidateCookie(r, c)
|
_, err = ValidateCookie(r, c)
|
||||||
assert.False(valid)
|
|
||||||
if assert.Error(err) {
|
if assert.Error(err) {
|
||||||
assert.Equal("Invalid cookie format", err.Error())
|
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)
|
_, err = ValidateCookie(r, c)
|
||||||
assert.False(valid)
|
|
||||||
if assert.Error(err) {
|
if assert.Error(err) {
|
||||||
assert.Equal("Invalid cookie mac", err.Error())
|
assert.Equal("Invalid cookie mac", err.Error())
|
||||||
}
|
}
|
||||||
@ -53,8 +49,7 @@ func TestAuthValidateCookie(t *testing.T) {
|
|||||||
// 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)
|
_, err = ValidateCookie(r, c)
|
||||||
assert.False(valid)
|
|
||||||
if assert.Error(err) {
|
if assert.Error(err) {
|
||||||
assert.Equal("Cookie has expired", err.Error())
|
assert.Equal("Cookie has expired", err.Error())
|
||||||
}
|
}
|
||||||
@ -62,8 +57,7 @@ func TestAuthValidateCookie(t *testing.T) {
|
|||||||
// 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)
|
email, err := ValidateCookie(r, c)
|
||||||
assert.True(valid, "valid request should return valid")
|
|
||||||
assert.Nil(err, "valid request should not return an error")
|
assert.Nil(err, "valid request should not return an error")
|
||||||
assert.Equal("test@test.com", email, "valid request should return user email")
|
assert.Equal("test@test.com", email, "valid request should return user email")
|
||||||
}
|
}
|
||||||
@ -72,168 +66,194 @@ func TestAuthValidateEmail(t *testing.T) {
|
|||||||
assert := assert.New(t)
|
assert := assert.New(t)
|
||||||
config, _ = NewConfig([]string{})
|
config, _ = NewConfig([]string{})
|
||||||
|
|
||||||
// Should allow any
|
// Should allow any with no whitelist/domain is specified
|
||||||
v := ValidateEmail("test@test.com")
|
v := ValidateEmail("test@test.com", "default")
|
||||||
assert.True(v, "should allow any domain if email domain is not defined")
|
assert.True(v, "should allow any domain if email domain is not defined")
|
||||||
v = ValidateEmail("one@two.com")
|
v = ValidateEmail("one@two.com", "default")
|
||||||
assert.True(v, "should allow any domain if email domain is not defined")
|
assert.True(v, "should allow any domain if email domain is not defined")
|
||||||
|
|
||||||
// Should block non matching domain
|
|
||||||
config.Domains = []string{"test.com"}
|
|
||||||
v = ValidateEmail("one@two.com")
|
|
||||||
assert.False(v, "should not allow user from another domain")
|
|
||||||
|
|
||||||
// Should allow matching domain
|
// Should allow matching domain
|
||||||
config.Domains = []string{"test.com"}
|
config.Domains = []string{"test.com"}
|
||||||
v = ValidateEmail("test@test.com")
|
v = ValidateEmail("one@two.com", "default")
|
||||||
|
assert.False(v, "should not allow user from another domain")
|
||||||
|
v = ValidateEmail("test@test.com", "default")
|
||||||
assert.True(v, "should allow user from allowed domain")
|
assert.True(v, "should allow user from allowed domain")
|
||||||
|
|
||||||
// Should block non whitelisted email address
|
|
||||||
config.Domains = []string{}
|
|
||||||
config.Whitelist = []string{"test@test.com"}
|
|
||||||
v = ValidateEmail("one@two.com")
|
|
||||||
assert.False(v, "should not allow user not in whitelist")
|
|
||||||
|
|
||||||
// Should allow matching whitelisted email address
|
// Should allow matching whitelisted email address
|
||||||
config.Domains = []string{}
|
config.Domains = []string{}
|
||||||
config.Whitelist = []string{"test@test.com"}
|
config.Whitelist = []string{"test@test.com"}
|
||||||
v = ValidateEmail("test@test.com")
|
v = ValidateEmail("one@two.com", "default")
|
||||||
|
assert.False(v, "should not allow user not in whitelist")
|
||||||
|
v = ValidateEmail("test@test.com", "default")
|
||||||
|
assert.True(v, "should allow user in whitelist")
|
||||||
|
|
||||||
|
// Should allow only matching email address when
|
||||||
|
// MatchWhitelistOrDomain is disabled
|
||||||
|
config.Domains = []string{"example.com"}
|
||||||
|
config.Whitelist = []string{"test@test.com"}
|
||||||
|
config.MatchWhitelistOrDomain = false
|
||||||
|
v = ValidateEmail("one@two.com", "default")
|
||||||
|
assert.False(v, "should not allow user not in either")
|
||||||
|
v = ValidateEmail("test@example.com", "default")
|
||||||
|
assert.False(v, "should not allow user from allowed domain")
|
||||||
|
v = ValidateEmail("test@test.com", "default")
|
||||||
|
assert.True(v, "should allow user in whitelist")
|
||||||
|
|
||||||
|
// Should allow either matching domain or email address when
|
||||||
|
// MatchWhitelistOrDomain is enabled
|
||||||
|
config.Domains = []string{"example.com"}
|
||||||
|
config.Whitelist = []string{"test@test.com"}
|
||||||
|
config.MatchWhitelistOrDomain = true
|
||||||
|
v = ValidateEmail("one@two.com", "default")
|
||||||
|
assert.False(v, "should not allow user not in either")
|
||||||
|
v = ValidateEmail("test@example.com", "default")
|
||||||
|
assert.True(v, "should allow user from allowed domain")
|
||||||
|
v = ValidateEmail("test@test.com", "default")
|
||||||
|
assert.True(v, "should allow user in whitelist")
|
||||||
|
|
||||||
|
// Rule testing
|
||||||
|
|
||||||
|
// Should use global whitelist/domain when not specified on rule
|
||||||
|
config.Domains = []string{"example.com"}
|
||||||
|
config.Whitelist = []string{"test@test.com"}
|
||||||
|
config.Rules = map[string]*Rule{"test": NewRule()}
|
||||||
|
config.MatchWhitelistOrDomain = true
|
||||||
|
v = ValidateEmail("one@two.com", "test")
|
||||||
|
assert.False(v, "should not allow user not in either")
|
||||||
|
v = ValidateEmail("test@example.com", "test")
|
||||||
|
assert.True(v, "should allow user from allowed global domain")
|
||||||
|
v = ValidateEmail("test@test.com", "test")
|
||||||
|
assert.True(v, "should allow user in global whitelist")
|
||||||
|
|
||||||
|
// Should allow matching domain in rule
|
||||||
|
config.Domains = []string{"testglobal.com"}
|
||||||
|
config.Whitelist = []string{}
|
||||||
|
rule := NewRule()
|
||||||
|
config.Rules = map[string]*Rule{"test": rule}
|
||||||
|
rule.Domains = []string{"testrule.com"}
|
||||||
|
config.MatchWhitelistOrDomain = false
|
||||||
|
v = ValidateEmail("one@two.com", "test")
|
||||||
|
assert.False(v, "should not allow user from another domain")
|
||||||
|
v = ValidateEmail("one@testglobal.com", "test")
|
||||||
|
assert.False(v, "should not allow user from global domain")
|
||||||
|
v = ValidateEmail("test@testrule.com", "test")
|
||||||
|
assert.True(v, "should allow user from allowed domain")
|
||||||
|
|
||||||
|
// Should allow matching whitelist in rule
|
||||||
|
config.Domains = []string{}
|
||||||
|
config.Whitelist = []string{"test@testglobal.com"}
|
||||||
|
rule = NewRule()
|
||||||
|
config.Rules = map[string]*Rule{"test": rule}
|
||||||
|
rule.Whitelist = []string{"test@testrule.com"}
|
||||||
|
config.MatchWhitelistOrDomain = false
|
||||||
|
v = ValidateEmail("one@two.com", "test")
|
||||||
|
assert.False(v, "should not allow user from another domain")
|
||||||
|
v = ValidateEmail("test@testglobal.com", "test")
|
||||||
|
assert.False(v, "should not allow user from global domain")
|
||||||
|
v = ValidateEmail("test@testrule.com", "test")
|
||||||
|
assert.True(v, "should allow user from allowed domain")
|
||||||
|
|
||||||
|
// Should allow only matching email address when
|
||||||
|
// MatchWhitelistOrDomain is disabled
|
||||||
|
config.Domains = []string{"exampleglobal.com"}
|
||||||
|
config.Whitelist = []string{"test@testglobal.com"}
|
||||||
|
rule = NewRule()
|
||||||
|
config.Rules = map[string]*Rule{"test": rule}
|
||||||
|
rule.Domains = []string{"examplerule.com"}
|
||||||
|
rule.Whitelist = []string{"test@testrule.com"}
|
||||||
|
config.MatchWhitelistOrDomain = false
|
||||||
|
v = ValidateEmail("one@two.com", "test")
|
||||||
|
assert.False(v, "should not allow user not in either")
|
||||||
|
v = ValidateEmail("test@testglobal.com", "test")
|
||||||
|
assert.False(v, "should not allow user in global whitelist")
|
||||||
|
v = ValidateEmail("test@exampleglobal.com", "test")
|
||||||
|
assert.False(v, "should not allow user from global domain")
|
||||||
|
v = ValidateEmail("test@examplerule.com", "test")
|
||||||
|
assert.False(v, "should not allow user from allowed domain")
|
||||||
|
v = ValidateEmail("test@testrule.com", "test")
|
||||||
|
assert.True(v, "should allow user in whitelist")
|
||||||
|
|
||||||
|
// Should allow either matching domain or email address when
|
||||||
|
// MatchWhitelistOrDomain is enabled
|
||||||
|
config.Domains = []string{"exampleglobal.com"}
|
||||||
|
config.Whitelist = []string{"test@testglobal.com"}
|
||||||
|
rule = NewRule()
|
||||||
|
config.Rules = map[string]*Rule{"test": rule}
|
||||||
|
rule.Domains = []string{"examplerule.com"}
|
||||||
|
rule.Whitelist = []string{"test@testrule.com"}
|
||||||
|
config.MatchWhitelistOrDomain = true
|
||||||
|
v = ValidateEmail("one@two.com", "test")
|
||||||
|
assert.False(v, "should not allow user not in either")
|
||||||
|
v = ValidateEmail("test@testglobal.com", "test")
|
||||||
|
assert.False(v, "should not allow user in global whitelist")
|
||||||
|
v = ValidateEmail("test@exampleglobal.com", "test")
|
||||||
|
assert.False(v, "should not allow user from global domain")
|
||||||
|
v = ValidateEmail("test@examplerule.com", "test")
|
||||||
|
assert.True(v, "should allow user from allowed domain")
|
||||||
|
v = ValidateEmail("test@testrule.com", "test")
|
||||||
assert.True(v, "should allow user in whitelist")
|
assert.True(v, "should allow user in whitelist")
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Split google tests out
|
func TestRedirectUri(t *testing.T) {
|
||||||
func TestAuthGetLoginURL(t *testing.T) {
|
|
||||||
assert := assert.New(t)
|
assert := assert.New(t)
|
||||||
google := provider.Google{
|
|
||||||
ClientId: "idtest",
|
|
||||||
ClientSecret: "sectest",
|
|
||||||
Scope: "scopetest",
|
|
||||||
Prompt: "consent select_account",
|
|
||||||
LoginURL: &url.URL{
|
|
||||||
Scheme: "https",
|
|
||||||
Host: "test.com",
|
|
||||||
Path: "/auth",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
config, _ = NewConfig([]string{})
|
r := httptest.NewRequest("GET", "http://app.example.com/hello", nil)
|
||||||
config.Providers.Google = google
|
|
||||||
|
|
||||||
r, _ := http.NewRequest("GET", "http://example.com", nil)
|
|
||||||
r.Header.Add("X-Forwarded-Proto", "http")
|
r.Header.Add("X-Forwarded-Proto", "http")
|
||||||
r.Header.Add("X-Forwarded-Host", "example.com")
|
|
||||||
r.Header.Add("X-Forwarded-Uri", "/hello")
|
|
||||||
|
|
||||||
// Check url
|
//
|
||||||
uri, err := url.Parse(GetLoginURL(r, "nonce"))
|
// No Auth Host
|
||||||
|
//
|
||||||
|
config, _ = NewConfig([]string{})
|
||||||
|
|
||||||
|
uri, err := url.Parse(redirectUri(r))
|
||||||
assert.Nil(err)
|
assert.Nil(err)
|
||||||
assert.Equal("https", uri.Scheme)
|
assert.Equal("http", uri.Scheme)
|
||||||
assert.Equal("test.com", uri.Host)
|
assert.Equal("app.example.com", uri.Host)
|
||||||
assert.Equal("/auth", uri.Path)
|
assert.Equal("/_oauth", uri.Path)
|
||||||
|
|
||||||
// Check query string
|
|
||||||
qs := uri.Query()
|
|
||||||
expectedQs := url.Values{
|
|
||||||
"client_id": []string{"idtest"},
|
|
||||||
"redirect_uri": []string{"http://example.com/_oauth"},
|
|
||||||
"response_type": []string{"code"},
|
|
||||||
"scope": []string{"scopetest"},
|
|
||||||
"prompt": []string{"consent select_account"},
|
|
||||||
"state": []string{"nonce:http://example.com/hello"},
|
|
||||||
}
|
|
||||||
assert.Equal(expectedQs, qs)
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// With Auth URL but no matching cookie domain
|
// With Auth URL but no matching cookie domain
|
||||||
// - will not use auth host
|
// - will not use auth host
|
||||||
//
|
//
|
||||||
config, _ = NewConfig([]string{})
|
|
||||||
config.AuthHost = "auth.example.com"
|
config.AuthHost = "auth.example.com"
|
||||||
config.Providers.Google = google
|
|
||||||
|
|
||||||
// Check url
|
uri, err = url.Parse(redirectUri(r))
|
||||||
uri, err = url.Parse(GetLoginURL(r, "nonce"))
|
|
||||||
assert.Nil(err)
|
assert.Nil(err)
|
||||||
assert.Equal("https", uri.Scheme)
|
assert.Equal("http", uri.Scheme)
|
||||||
assert.Equal("test.com", uri.Host)
|
assert.Equal("app.example.com", uri.Host)
|
||||||
assert.Equal("/auth", uri.Path)
|
assert.Equal("/_oauth", uri.Path)
|
||||||
|
|
||||||
// Check query string
|
|
||||||
qs = uri.Query()
|
|
||||||
expectedQs = url.Values{
|
|
||||||
"client_id": []string{"idtest"},
|
|
||||||
"redirect_uri": []string{"http://example.com/_oauth"},
|
|
||||||
"response_type": []string{"code"},
|
|
||||||
"scope": []string{"scopetest"},
|
|
||||||
"prompt": []string{"consent select_account"},
|
|
||||||
"state": []string{"nonce:http://example.com/hello"},
|
|
||||||
}
|
|
||||||
assert.Equal(expectedQs, qs)
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// With correct Auth URL + cookie domain
|
// With correct Auth URL + cookie domain
|
||||||
//
|
//
|
||||||
config, _ = NewConfig([]string{})
|
|
||||||
config.AuthHost = "auth.example.com"
|
config.AuthHost = "auth.example.com"
|
||||||
config.CookieDomains = []CookieDomain{*NewCookieDomain("example.com")}
|
config.CookieDomains = []CookieDomain{*NewCookieDomain("example.com")}
|
||||||
config.Providers.Google = google
|
|
||||||
|
|
||||||
// Check url
|
// Check url
|
||||||
uri, err = url.Parse(GetLoginURL(r, "nonce"))
|
uri, err = url.Parse(redirectUri(r))
|
||||||
assert.Nil(err)
|
assert.Nil(err)
|
||||||
assert.Equal("https", uri.Scheme)
|
assert.Equal("http", uri.Scheme)
|
||||||
assert.Equal("test.com", uri.Host)
|
assert.Equal("auth.example.com", uri.Host)
|
||||||
assert.Equal("/auth", uri.Path)
|
assert.Equal("/_oauth", uri.Path)
|
||||||
|
|
||||||
// Check query string
|
|
||||||
qs = uri.Query()
|
|
||||||
expectedQs = url.Values{
|
|
||||||
"client_id": []string{"idtest"},
|
|
||||||
"redirect_uri": []string{"http://auth.example.com/_oauth"},
|
|
||||||
"response_type": []string{"code"},
|
|
||||||
"scope": []string{"scopetest"},
|
|
||||||
"state": []string{"nonce:http://example.com/hello"},
|
|
||||||
"prompt": []string{"consent select_account"},
|
|
||||||
}
|
|
||||||
assert.Equal(expectedQs, qs)
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// With Auth URL + cookie domain, but from different domain
|
// With Auth URL + cookie domain, but from different domain
|
||||||
// - will not use auth host
|
// - will not use auth host
|
||||||
//
|
//
|
||||||
r, _ = http.NewRequest("GET", "http://another.com", nil)
|
r = httptest.NewRequest("GET", "https://another.com/hello", nil)
|
||||||
r.Header.Add("X-Forwarded-Proto", "http")
|
r.Header.Add("X-Forwarded-Proto", "https")
|
||||||
r.Header.Add("X-Forwarded-Host", "another.com")
|
|
||||||
r.Header.Add("X-Forwarded-Uri", "/hello")
|
config.AuthHost = "auth.example.com"
|
||||||
|
config.CookieDomains = []CookieDomain{*NewCookieDomain("example.com")}
|
||||||
|
|
||||||
// Check url
|
// Check url
|
||||||
uri, err = url.Parse(GetLoginURL(r, "nonce"))
|
uri, err = url.Parse(redirectUri(r))
|
||||||
assert.Nil(err)
|
assert.Nil(err)
|
||||||
assert.Equal("https", uri.Scheme)
|
assert.Equal("https", uri.Scheme)
|
||||||
assert.Equal("test.com", uri.Host)
|
assert.Equal("another.com", uri.Host)
|
||||||
assert.Equal("/auth", uri.Path)
|
assert.Equal("/_oauth", uri.Path)
|
||||||
|
|
||||||
// Check query string
|
|
||||||
qs = uri.Query()
|
|
||||||
expectedQs = url.Values{
|
|
||||||
"client_id": []string{"idtest"},
|
|
||||||
"redirect_uri": []string{"http://another.com/_oauth"},
|
|
||||||
"response_type": []string{"code"},
|
|
||||||
"scope": []string{"scopetest"},
|
|
||||||
"state": []string{"nonce:http://another.com/hello"},
|
|
||||||
"prompt": []string{"consent select_account"},
|
|
||||||
}
|
|
||||||
assert.Equal(expectedQs, qs)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO
|
|
||||||
// func TestAuthExchangeCode(t *testing.T) {
|
|
||||||
// }
|
|
||||||
|
|
||||||
// TODO
|
|
||||||
// func TestAuthGetUser(t *testing.T) {
|
|
||||||
// }
|
|
||||||
|
|
||||||
func TestAuthMakeCookie(t *testing.T) {
|
func TestAuthMakeCookie(t *testing.T) {
|
||||||
assert := assert.New(t)
|
assert := assert.New(t)
|
||||||
config, _ = NewConfig([]string{})
|
config, _ = NewConfig([]string{})
|
||||||
@ -244,8 +264,8 @@ func TestAuthMakeCookie(t *testing.T) {
|
|||||||
assert.Equal("_forward_auth", c.Name)
|
assert.Equal("_forward_auth", c.Name)
|
||||||
parts := strings.Split(c.Value, "|")
|
parts := strings.Split(c.Value, "|")
|
||||||
assert.Len(parts, 3, "cookie should be 3 parts")
|
assert.Len(parts, 3, "cookie should be 3 parts")
|
||||||
valid, _, _ := ValidateCookie(r, c)
|
_, err := ValidateCookie(r, c)
|
||||||
assert.True(valid, "should generate valid cookie")
|
assert.Nil(err, "should generate valid cookie")
|
||||||
assert.Equal("/", c.Path)
|
assert.Equal("/", c.Path)
|
||||||
assert.Equal("app.example.com", c.Domain)
|
assert.Equal("app.example.com", c.Domain)
|
||||||
assert.True(c.Secure)
|
assert.True(c.Secure)
|
||||||
@ -268,29 +288,30 @@ func TestAuthMakeCSRFCookie(t *testing.T) {
|
|||||||
|
|
||||||
// No cookie domain or auth url
|
// No cookie domain or auth url
|
||||||
c := MakeCSRFCookie(r, "12345678901234567890123456789012")
|
c := MakeCSRFCookie(r, "12345678901234567890123456789012")
|
||||||
|
assert.Equal("_forward_auth_csrf_123456", c.Name)
|
||||||
assert.Equal("app.example.com", c.Domain)
|
assert.Equal("app.example.com", c.Domain)
|
||||||
|
|
||||||
// With cookie domain but no auth url
|
// With cookie domain but no auth url
|
||||||
config = Config{
|
config.CookieDomains = []CookieDomain{*NewCookieDomain("example.com")}
|
||||||
CookieDomains: []CookieDomain{*NewCookieDomain("example.com")},
|
c = MakeCSRFCookie(r, "12222278901234567890123456789012")
|
||||||
}
|
assert.Equal("_forward_auth_csrf_122222", c.Name)
|
||||||
c = MakeCSRFCookie(r, "12345678901234567890123456789012")
|
|
||||||
assert.Equal("app.example.com", c.Domain)
|
assert.Equal("app.example.com", c.Domain)
|
||||||
|
|
||||||
// With cookie domain and auth url
|
// With cookie domain and auth url
|
||||||
config = Config{
|
config.AuthHost = "auth.example.com"
|
||||||
AuthHost: "auth.example.com",
|
config.CookieDomains = []CookieDomain{*NewCookieDomain("example.com")}
|
||||||
CookieDomains: []CookieDomain{*NewCookieDomain("example.com")},
|
c = MakeCSRFCookie(r, "12333378901234567890123456789012")
|
||||||
}
|
assert.Equal("_forward_auth_csrf_123333", c.Name)
|
||||||
c = MakeCSRFCookie(r, "12345678901234567890123456789012")
|
|
||||||
assert.Equal("example.com", c.Domain)
|
assert.Equal("example.com", c.Domain)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAuthClearCSRFCookie(t *testing.T) {
|
func TestAuthClearCSRFCookie(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
config, _ = NewConfig([]string{})
|
config, _ = NewConfig([]string{})
|
||||||
r, _ := http.NewRequest("GET", "http://example.com", nil)
|
r, _ := http.NewRequest("GET", "http://example.com", nil)
|
||||||
|
|
||||||
c := ClearCSRFCookie(r)
|
c := ClearCSRFCookie(r, &http.Cookie{Name: "someCsrfCookie"})
|
||||||
|
assert.Equal("someCsrfCookie", c.Name)
|
||||||
if c.Value != "" {
|
if c.Value != "" {
|
||||||
t.Error("ClearCSRFCookie should create cookie with empty value")
|
t.Error("ClearCSRFCookie should create cookie with empty value")
|
||||||
}
|
}
|
||||||
@ -300,44 +321,77 @@ func TestAuthValidateCSRFCookie(t *testing.T) {
|
|||||||
assert := assert.New(t)
|
assert := assert.New(t)
|
||||||
config, _ = NewConfig([]string{})
|
config, _ = NewConfig([]string{})
|
||||||
c := &http.Cookie{}
|
c := &http.Cookie{}
|
||||||
|
state := ""
|
||||||
newCsrfRequest := func(state string) *http.Request {
|
|
||||||
u := fmt.Sprintf("http://example.com?state=%s", state)
|
|
||||||
r, _ := http.NewRequest("GET", u, nil)
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
|
|
||||||
// Should require 32 char string
|
// Should require 32 char string
|
||||||
r := newCsrfRequest("")
|
state = ""
|
||||||
c.Value = ""
|
c.Value = ""
|
||||||
valid, _, err := ValidateCSRFCookie(r, c)
|
valid, _, _, err := ValidateCSRFCookie(c, state)
|
||||||
assert.False(valid)
|
assert.False(valid)
|
||||||
if assert.Error(err) {
|
if assert.Error(err) {
|
||||||
assert.Equal("Invalid CSRF cookie value", err.Error())
|
assert.Equal("Invalid CSRF cookie value", err.Error())
|
||||||
}
|
}
|
||||||
c.Value = "123456789012345678901234567890123"
|
c.Value = "123456789012345678901234567890123"
|
||||||
valid, _, err = ValidateCSRFCookie(r, c)
|
valid, _, _, err = ValidateCSRFCookie(c, state)
|
||||||
assert.False(valid)
|
assert.False(valid)
|
||||||
if assert.Error(err) {
|
if assert.Error(err) {
|
||||||
assert.Equal("Invalid CSRF cookie value", err.Error())
|
assert.Equal("Invalid CSRF cookie value", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Should require valid state
|
// Should require provider
|
||||||
r = newCsrfRequest("12345678901234567890123456789012:")
|
state = "12345678901234567890123456789012:99"
|
||||||
c.Value = "12345678901234567890123456789012"
|
c.Value = "12345678901234567890123456789012"
|
||||||
valid, _, err = ValidateCSRFCookie(r, c)
|
valid, _, _, err = ValidateCSRFCookie(c, state)
|
||||||
assert.False(valid)
|
assert.False(valid)
|
||||||
if assert.Error(err) {
|
if assert.Error(err) {
|
||||||
assert.Equal("Invalid CSRF state value", err.Error())
|
assert.Equal("Invalid CSRF state format", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Should allow valid state
|
// Should allow valid state
|
||||||
r = newCsrfRequest("12345678901234567890123456789012:99")
|
state = "12345678901234567890123456789012:p99:url123"
|
||||||
c.Value = "12345678901234567890123456789012"
|
c.Value = "12345678901234567890123456789012"
|
||||||
valid, state, err := ValidateCSRFCookie(r, c)
|
valid, provider, redirect, err := ValidateCSRFCookie(c, state)
|
||||||
assert.True(valid, "valid request should return valid")
|
assert.True(valid, "valid request should return valid")
|
||||||
assert.Nil(err, "valid request should not return an error")
|
assert.Nil(err, "valid request should not return an error")
|
||||||
assert.Equal("99", state, "valid request should return correct state")
|
assert.Equal("p99", provider, "valid request should return correct provider")
|
||||||
|
assert.Equal("url123", redirect, "valid request should return correct redirect")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateState(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
// Should require valid state
|
||||||
|
state := "12345678901234567890123456789012:"
|
||||||
|
err := ValidateState(state)
|
||||||
|
if assert.Error(err) {
|
||||||
|
assert.Equal("Invalid CSRF state value", err.Error())
|
||||||
|
}
|
||||||
|
// Should pass this state
|
||||||
|
state = "12345678901234567890123456789012:p99:url123"
|
||||||
|
err = ValidateState(state)
|
||||||
|
assert.Nil(err, "valid request should not return an error")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMakeState(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
r := httptest.NewRequest("GET", "http://example.com/hello", nil)
|
||||||
|
r.Header.Add("X-Forwarded-Proto", "http")
|
||||||
|
|
||||||
|
// Test with google
|
||||||
|
p := provider.Google{}
|
||||||
|
state := MakeState(r, &p, "nonce")
|
||||||
|
assert.Equal("nonce:google:http://example.com/hello", state)
|
||||||
|
|
||||||
|
// Test with OIDC
|
||||||
|
p2 := provider.OIDC{}
|
||||||
|
state = MakeState(r, &p2, "nonce")
|
||||||
|
assert.Equal("nonce:oidc:http://example.com/hello", state)
|
||||||
|
|
||||||
|
// Test with Generic OAuth
|
||||||
|
p3 := provider.GenericOAuth{}
|
||||||
|
state = MakeState(r, &p3, "nonce")
|
||||||
|
assert.Equal("nonce:generic-oauth:http://example.com/hello", state)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAuthNonce(t *testing.T) {
|
func TestAuthNonce(t *testing.T) {
|
||||||
@ -362,6 +416,8 @@ func TestAuthCookieDomainMatch(t *testing.T) {
|
|||||||
|
|
||||||
// Subdomain should match
|
// Subdomain should match
|
||||||
assert.True(cd.Match("test.example.com"), "subdomain should match")
|
assert.True(cd.Match("test.example.com"), "subdomain should match")
|
||||||
|
assert.True(cd.Match("twolevels.test.example.com"), "subdomain should match")
|
||||||
|
assert.True(cd.Match("many.many.levels.test.example.com"), "subdomain should match")
|
||||||
|
|
||||||
// Derived domain should not match
|
// Derived domain should not match
|
||||||
assert.False(cd.Match("testexample.com"), "derived domain should not match")
|
assert.False(cd.Match("testexample.com"), "derived domain should not match")
|
||||||
|
@ -7,54 +7,61 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/url"
|
|
||||||
"os"
|
"os"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/jessevdk/go-flags"
|
"github.com/thomseddon/go-flags"
|
||||||
"github.com/thomseddon/traefik-forward-auth/internal/provider"
|
"github.com/thomseddon/traefik-forward-auth/internal/provider"
|
||||||
)
|
)
|
||||||
|
|
||||||
var config Config
|
var config *Config
|
||||||
|
|
||||||
|
// Config holds the runtime application config
|
||||||
type Config struct {
|
type Config struct {
|
||||||
LogLevel string `long:"log-level" env:"LOG_LEVEL" default:"warn" choice:"trace" choice:"debug" choice:"info" choice:"warn" choice:"error" choice:"fatal" choice:"panic" description:"Log level"`
|
LogLevel string `long:"log-level" env:"LOG_LEVEL" default:"warn" choice:"trace" choice:"debug" choice:"info" choice:"warn" choice:"error" choice:"fatal" choice:"panic" description:"Log level"`
|
||||||
LogFormat string `long:"log-format" env:"LOG_FORMAT" default:"text" choice:"text" choice:"json" choice:"pretty" description:"Log format"`
|
LogFormat string `long:"log-format" env:"LOG_FORMAT" default:"text" choice:"text" choice:"json" choice:"pretty" description:"Log format"`
|
||||||
|
|
||||||
AuthHost string `long:"auth-host" env:"AUTH_HOST" description:"Single host to use when returning from 3rd party auth"`
|
AuthHost string `long:"auth-host" env:"AUTH_HOST" description:"Single host to use when returning from 3rd party auth"`
|
||||||
Config func(s string) error `long:"config" env:"CONFIG" description:"Path to config file"`
|
Config func(s string) error `long:"config" env:"CONFIG" description:"Path to config file" json:"-"`
|
||||||
CookieDomains []CookieDomain `long:"cookie-domain" env:"COOKIE_DOMAIN" description:"Domain to set auth cookie on, can be set multiple times"`
|
CookieDomains []CookieDomain `long:"cookie-domain" env:"COOKIE_DOMAIN" env-delim:"," description:"Domain to set auth cookie on, can be set multiple times"`
|
||||||
InsecureCookie bool `long:"insecure-cookie" env:"INSECURE_COOKIE" description:"Use insecure cookies"`
|
InsecureCookie bool `long:"insecure-cookie" env:"INSECURE_COOKIE" description:"Use insecure cookies"`
|
||||||
CookieName string `long:"cookie-name" env:"COOKIE_NAME" default:"_forward_auth" description:"Cookie Name"`
|
CookieName string `long:"cookie-name" env:"COOKIE_NAME" default:"_forward_auth" description:"Cookie Name"`
|
||||||
CSRFCookieName string `long:"csrf-cookie-name" env:"CSRF_COOKIE_NAME" default:"_forward_auth_csrf" description:"CSRF Cookie Name"`
|
CSRFCookieName string `long:"csrf-cookie-name" env:"CSRF_COOKIE_NAME" default:"_forward_auth_csrf" description:"CSRF Cookie Name"`
|
||||||
DefaultAction string `long:"default-action" env:"DEFAULT_ACTION" default:"auth" choice:"auth" choice:"allow" description:"Default action"`
|
DefaultAction string `long:"default-action" env:"DEFAULT_ACTION" default:"auth" choice:"auth" choice:"allow" description:"Default action"`
|
||||||
Domains []string `long:"domain" env:"DOMAIN" description:"Only allow given email domains, can be set multiple times"`
|
DefaultProvider string `long:"default-provider" env:"DEFAULT_PROVIDER" default:"google" choice:"google" choice:"oidc" choice:"generic-oauth" description:"Default provider"`
|
||||||
|
Domains CommaSeparatedList `long:"domain" env:"DOMAIN" env-delim:"," description:"Only allow given email domains, can be set multiple times"`
|
||||||
LifetimeString int `long:"lifetime" env:"LIFETIME" default:"43200" description:"Lifetime in seconds"`
|
LifetimeString int `long:"lifetime" env:"LIFETIME" default:"43200" description:"Lifetime in seconds"`
|
||||||
|
LogoutRedirect string `long:"logout-redirect" env:"LOGOUT_REDIRECT" description:"URL to redirect to following logout"`
|
||||||
|
MatchWhitelistOrDomain bool `long:"match-whitelist-or-domain" env:"MATCH_WHITELIST_OR_DOMAIN" description:"Allow users that match *either* whitelist or domain (enabled by default in v3)"`
|
||||||
Path string `long:"url-path" env:"URL_PATH" default:"/_oauth" description:"Callback URL Path"`
|
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"`
|
Whitelist CommaSeparatedList `long:"whitelist" env:"WHITELIST" env-delim:"," description:"Only allow given email addresses, can be set multiple times"`
|
||||||
|
Port int `long:"port" env:"PORT" default:"4181" description:"Port to listen on"`
|
||||||
|
|
||||||
Providers provider.Providers `group:"providers" namespace:"providers" env-namespace:"PROVIDERS"`
|
Providers provider.Providers `group:"providers" namespace:"providers" env-namespace:"PROVIDERS"`
|
||||||
Rules map[string]*Rule `long:"rules.<name>.<param>" description:"Rule definitions, param can be: \"action\" or \"rule\""`
|
Rules map[string]*Rule `long:"rule.<name>.<param>" description:"Rule definitions, param can be: \"action\", \"rule\" or \"provider\""`
|
||||||
|
|
||||||
// Filled during transformations
|
// Filled during transformations
|
||||||
Secret []byte
|
Secret []byte `json:"-"`
|
||||||
Lifetime time.Duration
|
Lifetime time.Duration
|
||||||
|
|
||||||
|
// Authorization
|
||||||
|
RequiredRole string `long:"required-role" env:"REQUIRED_ROLE" description:"Required role to verify authorization"`
|
||||||
|
|
||||||
// Legacy
|
// Legacy
|
||||||
CookieDomainsLegacy CookieDomains `long:"cookie-domains" env:"COOKIE_DOMAINS" description:"DEPRECATED - Use \"cookie-domain\""`
|
CookieDomainsLegacy CookieDomains `long:"cookie-domains" env:"COOKIE_DOMAINS" description:"DEPRECATED - Use \"cookie-domain\""`
|
||||||
CookieSecretLegacy string `long:"cookie-secret" env:"COOKIE_SECRET" description:"DEPRECATED - Use \"secret\""`
|
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\""`
|
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" 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\"" json:"-"`
|
||||||
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\""`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewGlobalConfig() Config {
|
// NewGlobalConfig creates a new global config, parsed from command arguments
|
||||||
|
func NewGlobalConfig() *Config {
|
||||||
var err error
|
var err error
|
||||||
config, err = NewConfig(os.Args[1:])
|
config, err = NewConfig(os.Args[1:])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -65,29 +72,12 @@ func NewGlobalConfig() Config {
|
|||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewConfig(args []string) (Config, error) {
|
// TODO: move config parsing into new func "NewParsedConfig"
|
||||||
c := Config{
|
|
||||||
|
// NewConfig parses and validates provided configuration into a config object
|
||||||
|
func NewConfig(args []string) (*Config, error) {
|
||||||
|
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)
|
||||||
@ -98,20 +88,33 @@ func NewConfig(args []string) (Config, error) {
|
|||||||
// TODO: as log flags have now been parsed maybe we should return here so
|
// TODO: as log flags have now been parsed maybe we should return here so
|
||||||
// any further errors can be logged via logrus instead of printed?
|
// any further errors can be logged via logrus instead of printed?
|
||||||
|
|
||||||
|
// TODO: Rename "Validate" method to "Setup" and move all below logic
|
||||||
|
|
||||||
|
// Setup
|
||||||
|
// Set default provider on any rules where it's not specified
|
||||||
|
for _, rule := range c.Rules {
|
||||||
|
if rule.Provider == "" {
|
||||||
|
rule.Provider = c.DefaultProvider
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Backwards compatability
|
// Backwards compatability
|
||||||
if c.CookieSecretLegacy != "" && c.SecretString == "" {
|
if c.CookieSecretLegacy != "" && c.SecretString == "" {
|
||||||
|
fmt.Println("cookie-secret config option is deprecated, please use secret")
|
||||||
c.SecretString = c.CookieSecretLegacy
|
c.SecretString = c.CookieSecretLegacy
|
||||||
}
|
}
|
||||||
if c.ClientIdLegacy != "" {
|
if c.ClientIdLegacy != "" {
|
||||||
c.Providers.Google.ClientId = c.ClientIdLegacy
|
c.Providers.Google.ClientID = c.ClientIdLegacy
|
||||||
}
|
}
|
||||||
if c.ClientSecretLegacy != "" {
|
if c.ClientSecretLegacy != "" {
|
||||||
c.Providers.Google.ClientSecret = c.ClientSecretLegacy
|
c.Providers.Google.ClientSecret = c.ClientSecretLegacy
|
||||||
}
|
}
|
||||||
if c.PromptLegacy != "" {
|
if c.PromptLegacy != "" {
|
||||||
|
fmt.Println("prompt config option is deprecated, please use providers.google.prompt")
|
||||||
c.Providers.Google.Prompt = c.PromptLegacy
|
c.Providers.Google.Prompt = c.PromptLegacy
|
||||||
}
|
}
|
||||||
if c.CookieSecureLegacy != "" {
|
if c.CookieSecureLegacy != "" {
|
||||||
|
fmt.Println("cookie-secure config option is deprecated, please use insecure-cookie")
|
||||||
secure, err := strconv.ParseBool(c.CookieSecureLegacy)
|
secure, err := strconv.ParseBool(c.CookieSecureLegacy)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return c, err
|
return c, err
|
||||||
@ -119,11 +122,9 @@ func NewConfig(args []string) (Config, error) {
|
|||||||
c.InsecureCookie = !secure
|
c.InsecureCookie = !secure
|
||||||
}
|
}
|
||||||
if len(c.CookieDomainsLegacy) > 0 {
|
if len(c.CookieDomainsLegacy) > 0 {
|
||||||
|
fmt.Println("cookie-domains config option is deprecated, please use cookie-domain")
|
||||||
c.CookieDomains = append(c.CookieDomains, c.CookieDomainsLegacy...)
|
c.CookieDomains = append(c.CookieDomains, c.CookieDomainsLegacy...)
|
||||||
}
|
}
|
||||||
if len(c.DomainsLegacy) > 0 {
|
|
||||||
c.Domains = append(c.Domains, c.DomainsLegacy...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Transformations
|
// Transformations
|
||||||
if len(c.Path) > 0 && c.Path[0] != '/' {
|
if len(c.Path) > 0 && c.Path[0] != '/' {
|
||||||
@ -136,7 +137,7 @@ func NewConfig(args []string) (Config, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Config) parseFlags(args []string) 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
|
p.UnknownOptionHandler = c.parseUnknownFlag
|
||||||
|
|
||||||
i := flags.NewIniParser(p)
|
i := flags.NewIniParser(p)
|
||||||
@ -152,6 +153,7 @@ func (c *Config) parseFlags(args []string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fmt.Println("config format deprecated, please use ini format")
|
||||||
return i.Parse(converted)
|
return i.Parse(converted)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -160,7 +162,7 @@ func (c *Config) parseFlags(args []string) error {
|
|||||||
|
|
||||||
_, err := p.ParseArgs(args)
|
_, err := p.ParseArgs(args)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return handlFlagError(err)
|
return handleFlagError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@ -170,16 +172,15 @@ func (c *Config) parseUnknownFlag(option string, arg flags.SplitArgument, args [
|
|||||||
// Parse rules in the format "rule.<name>.<param>"
|
// Parse rules in the format "rule.<name>.<param>"
|
||||||
parts := strings.Split(option, ".")
|
parts := strings.Split(option, ".")
|
||||||
if len(parts) == 3 && parts[0] == "rule" {
|
if len(parts) == 3 && parts[0] == "rule" {
|
||||||
// Get or create rule
|
// Ensure there is a name
|
||||||
rule, ok := c.Rules[parts[1]]
|
name := parts[1]
|
||||||
if !ok {
|
if len(name) == 0 {
|
||||||
rule = NewRule()
|
return args, errors.New("route name is required")
|
||||||
c.Rules[parts[1]] = rule
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get value, or pop the next arg
|
// Get value, or pop the next arg
|
||||||
val, ok := arg.Value()
|
val, ok := arg.Value()
|
||||||
if !ok {
|
if !ok && len(args) > 1 {
|
||||||
val = args[0]
|
val = args[0]
|
||||||
args = args[1:]
|
args = args[1:]
|
||||||
}
|
}
|
||||||
@ -198,6 +199,13 @@ func (c *Config) parseUnknownFlag(option string, arg flags.SplitArgument, args [
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get or create rule
|
||||||
|
rule, ok := c.Rules[name]
|
||||||
|
if !ok {
|
||||||
|
rule = NewRule()
|
||||||
|
c.Rules[name] = rule
|
||||||
|
}
|
||||||
|
|
||||||
// Add param value to rule
|
// Add param value to rule
|
||||||
switch parts[2] {
|
switch parts[2] {
|
||||||
case "action":
|
case "action":
|
||||||
@ -206,8 +214,16 @@ func (c *Config) parseUnknownFlag(option string, arg flags.SplitArgument, args [
|
|||||||
rule.Rule = val
|
rule.Rule = val
|
||||||
case "provider":
|
case "provider":
|
||||||
rule.Provider = val
|
rule.Provider = val
|
||||||
|
case "whitelist":
|
||||||
|
list := CommaSeparatedList{}
|
||||||
|
list.UnmarshalFlag(val)
|
||||||
|
rule.Whitelist = list
|
||||||
|
case "domains":
|
||||||
|
list := CommaSeparatedList{}
|
||||||
|
list.UnmarshalFlag(val)
|
||||||
|
rule.Domains = list
|
||||||
default:
|
default:
|
||||||
return args, fmt.Errorf("inavlid route param: %v", option)
|
return args, fmt.Errorf("invalid route param: %v", option)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return args, fmt.Errorf("unknown flag: %v", option)
|
return args, fmt.Errorf("unknown flag: %v", option)
|
||||||
@ -216,7 +232,7 @@ func (c *Config) parseUnknownFlag(option string, arg flags.SplitArgument, args [
|
|||||||
return args, nil
|
return args, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func handlFlagError(err error) error {
|
func handleFlagError(err error) error {
|
||||||
flagsErr, ok := err.(*flags.Error)
|
flagsErr, ok := err.(*flags.Error)
|
||||||
if ok && flagsErr.Type == flags.ErrHelp {
|
if ok && flagsErr.Type == flags.ErrHelp {
|
||||||
// Library has just printed cli help
|
// Library has just printed cli help
|
||||||
@ -237,19 +253,25 @@ func convertLegacyToIni(name string) (io.Reader, error) {
|
|||||||
return bytes.NewReader(legacyFileFormat.ReplaceAll(b, []byte("$1=$2"))), nil
|
return bytes.NewReader(legacyFileFormat.ReplaceAll(b, []byte("$1=$2"))), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate validates a config object
|
||||||
func (c *Config) Validate() {
|
func (c *Config) Validate() {
|
||||||
// Check for show stopper errors
|
// Check for show stopper errors
|
||||||
if len(c.Secret) == 0 {
|
if len(c.Secret) == 0 {
|
||||||
log.Fatal("\"secret\" option must be set.")
|
log.Fatal("\"secret\" option must be set")
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.Providers.Google.ClientId == "" || c.Providers.Google.ClientSecret == "" {
|
// Setup default provider
|
||||||
log.Fatal("google.providers.client-id, google.providers.client-secret must be set")
|
err := c.setupProvider(c.DefaultProvider)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check rules
|
// Check rules (validates the rule and the rule provider)
|
||||||
for _, rule := range c.Rules {
|
for _, rule := range c.Rules {
|
||||||
rule.Validate()
|
err = rule.Validate(c)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -258,39 +280,106 @@ func (c Config) String() string {
|
|||||||
return string(jsonConf)
|
return string(jsonConf)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetProvider returns the provider of the given name
|
||||||
|
func (c *Config) GetProvider(name string) (provider.Provider, error) {
|
||||||
|
switch name {
|
||||||
|
case "google":
|
||||||
|
return &c.Providers.Google, nil
|
||||||
|
case "oidc":
|
||||||
|
return &c.Providers.OIDC, nil
|
||||||
|
case "generic-oauth":
|
||||||
|
return &c.Providers.GenericOAuth, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("Unknown provider: %s", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetConfiguredProvider returns the provider of the given name, if it has been
|
||||||
|
// configured. Returns an error if the provider is unknown, or hasn't been configured
|
||||||
|
func (c *Config) GetConfiguredProvider(name string) (provider.Provider, error) {
|
||||||
|
// Check the provider has been configured
|
||||||
|
if !c.providerConfigured(name) {
|
||||||
|
return nil, fmt.Errorf("Unconfigured provider: %s", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.GetProvider(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) providerConfigured(name string) bool {
|
||||||
|
// Check default provider
|
||||||
|
if name == c.DefaultProvider {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check rule providers
|
||||||
|
for _, rule := range c.Rules {
|
||||||
|
if name == rule.Provider {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) setupProvider(name string) error {
|
||||||
|
// Check provider exists
|
||||||
|
p, err := c.GetProvider(name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup
|
||||||
|
err = p.Setup(log)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rule holds defined rules
|
||||||
type Rule struct {
|
type Rule struct {
|
||||||
Action string
|
Action string
|
||||||
Rule string
|
Rule string
|
||||||
Provider string
|
Provider string
|
||||||
|
Whitelist CommaSeparatedList
|
||||||
|
Domains CommaSeparatedList
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewRule creates a new rule object
|
||||||
func NewRule() *Rule {
|
func NewRule() *Rule {
|
||||||
return &Rule{
|
return &Rule{
|
||||||
Action: "auth",
|
Action: "auth",
|
||||||
Provider: "google", // TODO: Use default provider
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Rule) Validate() {
|
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(")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate validates a rule
|
||||||
|
func (r *Rule) Validate(c *Config) error {
|
||||||
if r.Action != "auth" && r.Action != "allow" {
|
if r.Action != "auth" && r.Action != "allow" {
|
||||||
log.Fatal("invalid rule action, must be \"auth\" or \"allow\"")
|
return errors.New("invalid rule action, must be \"auth\" or \"allow\"")
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Update with more provider support
|
return c.setupProvider(r.Provider)
|
||||||
if r.Provider != "google" {
|
|
||||||
log.Fatal("invalid rule provider, must be \"google\"")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Legacy support for comma separated lists
|
// Legacy support for comma separated lists
|
||||||
|
|
||||||
|
// CommaSeparatedList provides legacy support for config values provided as csv
|
||||||
type CommaSeparatedList []string
|
type CommaSeparatedList []string
|
||||||
|
|
||||||
|
// UnmarshalFlag converts a comma separated list to an array
|
||||||
func (c *CommaSeparatedList) UnmarshalFlag(value string) error {
|
func (c *CommaSeparatedList) UnmarshalFlag(value string) error {
|
||||||
*c = append(*c, strings.Split(value, ",")...)
|
*c = append(*c, strings.Split(value, ",")...)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MarshalFlag converts an array back to a comma separated list
|
||||||
func (c *CommaSeparatedList) MarshalFlag() (string, error) {
|
func (c *CommaSeparatedList) MarshalFlag() (string, error) {
|
||||||
return strings.Join(*c, ","), nil
|
return strings.Join(*c, ","), nil
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
package tfa
|
package tfa
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/url"
|
// "fmt"
|
||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"github.com/sirupsen/logrus/hooks/test"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
@ -28,34 +30,16 @@ func TestConfigDefaults(t *testing.T) {
|
|||||||
assert.Equal("_forward_auth", c.CookieName)
|
assert.Equal("_forward_auth", c.CookieName)
|
||||||
assert.Equal("_forward_auth_csrf", c.CSRFCookieName)
|
assert.Equal("_forward_auth_csrf", c.CSRFCookieName)
|
||||||
assert.Equal("auth", c.DefaultAction)
|
assert.Equal("auth", c.DefaultAction)
|
||||||
|
assert.Equal("google", c.DefaultProvider)
|
||||||
assert.Len(c.Domains, 0)
|
assert.Len(c.Domains, 0)
|
||||||
assert.Equal(time.Second*time.Duration(43200), c.Lifetime)
|
assert.Equal(time.Second*time.Duration(43200), c.Lifetime)
|
||||||
|
assert.Equal("", c.LogoutRedirect)
|
||||||
|
assert.False(c.MatchWhitelistOrDomain)
|
||||||
assert.Equal("/_oauth", c.Path)
|
assert.Equal("/_oauth", c.Path)
|
||||||
assert.Len(c.Whitelist, 0)
|
assert.Len(c.Whitelist, 0)
|
||||||
|
assert.Equal(c.Port, 4181)
|
||||||
|
|
||||||
assert.Equal("https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email", c.Providers.Google.Scope)
|
assert.Equal("select_account", c.Providers.Google.Prompt)
|
||||||
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{
|
|
||||||
Scheme: "https",
|
|
||||||
Host: "www.googleapis.com",
|
|
||||||
Path: "/oauth2/v3/token",
|
|
||||||
}
|
|
||||||
assert.Equal(tokenURL, c.Providers.Google.TokenURL)
|
|
||||||
|
|
||||||
userURL := &url.URL{
|
|
||||||
Scheme: "https",
|
|
||||||
Host: "www.googleapis.com",
|
|
||||||
Path: "/oauth2/v2/userinfo",
|
|
||||||
}
|
|
||||||
assert.Equal(userURL, c.Providers.Google.UserURL)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestConfigParseArgs(t *testing.T) {
|
func TestConfigParseArgs(t *testing.T) {
|
||||||
@ -63,28 +47,32 @@ func TestConfigParseArgs(t *testing.T) {
|
|||||||
c, err := NewConfig([]string{
|
c, err := NewConfig([]string{
|
||||||
"--cookie-name=cookiename",
|
"--cookie-name=cookiename",
|
||||||
"--csrf-cookie-name", "\"csrfcookiename\"",
|
"--csrf-cookie-name", "\"csrfcookiename\"",
|
||||||
|
"--default-provider", "\"oidc\"",
|
||||||
"--rule.1.action=allow",
|
"--rule.1.action=allow",
|
||||||
"--rule.1.rule=PathPrefix(`/one`)",
|
"--rule.1.rule=PathPrefix(`/one`)",
|
||||||
"--rule.two.action=auth",
|
"--rule.two.action=auth",
|
||||||
"--rule.two.rule=\"Host(`two.com`) && Path(`/two`)\"",
|
"--rule.two.rule=\"Host(`two.com`) && Path(`/two`)\"",
|
||||||
|
"--port=8000",
|
||||||
})
|
})
|
||||||
require.Nil(t, err)
|
require.Nil(t, err)
|
||||||
|
|
||||||
// Check normal flags
|
// Check normal flags
|
||||||
assert.Equal("cookiename", c.CookieName)
|
assert.Equal("cookiename", c.CookieName)
|
||||||
assert.Equal("csrfcookiename", c.CSRFCookieName)
|
assert.Equal("csrfcookiename", c.CSRFCookieName)
|
||||||
|
assert.Equal("oidc", c.DefaultProvider)
|
||||||
|
assert.Equal(8000, c.Port)
|
||||||
|
|
||||||
// Check rules
|
// Check rules
|
||||||
assert.Equal(map[string]*Rule{
|
assert.Equal(map[string]*Rule{
|
||||||
"1": {
|
"1": {
|
||||||
Action: "allow",
|
Action: "allow",
|
||||||
Rule: "PathPrefix(`/one`)",
|
Rule: "PathPrefix(`/one`)",
|
||||||
Provider: "google",
|
Provider: "oidc",
|
||||||
},
|
},
|
||||||
"two": {
|
"two": {
|
||||||
Action: "auth",
|
Action: "auth",
|
||||||
Rule: "Host(`two.com`) && Path(`/two`)",
|
Rule: "Host(`two.com`) && Path(`/two`)",
|
||||||
Provider: "google",
|
Provider: "oidc",
|
||||||
},
|
},
|
||||||
}, c.Rules)
|
}, c.Rules)
|
||||||
}
|
}
|
||||||
@ -98,6 +86,28 @@ func TestConfigParseUnknownFlags(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestConfigParseRuleError(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
// Rule without name
|
||||||
|
_, err := NewConfig([]string{
|
||||||
|
"--rule..action=auth",
|
||||||
|
})
|
||||||
|
if assert.Error(err) {
|
||||||
|
assert.Equal("route name is required", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rule without value
|
||||||
|
c, err := NewConfig([]string{
|
||||||
|
"--rule.one.action=",
|
||||||
|
})
|
||||||
|
if assert.Error(err) {
|
||||||
|
assert.Equal("route param value is required", err.Error())
|
||||||
|
}
|
||||||
|
// Check rules
|
||||||
|
assert.Equal(map[string]*Rule{}, c.Rules)
|
||||||
|
}
|
||||||
|
|
||||||
func TestConfigFlagBackwardsCompatability(t *testing.T) {
|
func TestConfigFlagBackwardsCompatability(t *testing.T) {
|
||||||
assert := assert.New(t)
|
assert := assert.New(t)
|
||||||
c, err := NewConfig([]string{
|
c, err := NewConfig([]string{
|
||||||
@ -109,7 +119,7 @@ func TestConfigFlagBackwardsCompatability(t *testing.T) {
|
|||||||
"--cookie-secure=false",
|
"--cookie-secure=false",
|
||||||
"--cookie-domains=test1.com,example.org",
|
"--cookie-domains=test1.com,example.org",
|
||||||
"--cookie-domain=another1.net",
|
"--cookie-domain=another1.net",
|
||||||
"--domains=test2.com,example.org",
|
"--domain=test2.com,example.org",
|
||||||
"--domain=another2.net",
|
"--domain=another2.net",
|
||||||
"--whitelist=test3.com,example.org",
|
"--whitelist=test3.com,example.org",
|
||||||
"--whitelist=another3.net",
|
"--whitelist=another3.net",
|
||||||
@ -124,7 +134,7 @@ func TestConfigFlagBackwardsCompatability(t *testing.T) {
|
|||||||
}
|
}
|
||||||
assert.Equal(expected1, c.CookieDomains, "should read legacy comma separated list cookie-domains")
|
assert.Equal(expected1, c.CookieDomains, "should read legacy comma separated list cookie-domains")
|
||||||
|
|
||||||
expected2 := []string{"another2.net", "test2.com", "example.org"}
|
expected2 := CommaSeparatedList{"test2.com", "example.org", "another2.net"}
|
||||||
assert.Equal(expected2, c.Domains, "should read legacy comma separated list domains")
|
assert.Equal(expected2, c.Domains, "should read legacy comma separated list domains")
|
||||||
|
|
||||||
expected3 := CommaSeparatedList{"test3.com", "example.org", "another3.net"}
|
expected3 := CommaSeparatedList{"test3.com", "example.org", "another3.net"}
|
||||||
@ -135,7 +145,7 @@ func TestConfigFlagBackwardsCompatability(t *testing.T) {
|
|||||||
|
|
||||||
// Google provider params used to be top level
|
// Google provider params used to be top level
|
||||||
assert.Equal("clientid", c.ClientIdLegacy)
|
assert.Equal("clientid", c.ClientIdLegacy)
|
||||||
assert.Equal("clientid", c.Providers.Google.ClientId, "--client-id should set providers.google.client-id")
|
assert.Equal("clientid", c.Providers.Google.ClientID, "--client-id should set providers.google.client-id")
|
||||||
assert.Equal("verysecret", c.ClientSecretLegacy)
|
assert.Equal("verysecret", c.ClientSecretLegacy)
|
||||||
assert.Equal("verysecret", c.Providers.Google.ClientSecret, "--client-secret should set providers.google.client-secret")
|
assert.Equal("verysecret", c.Providers.Google.ClientSecret, "--client-secret should set providers.google.client-secret")
|
||||||
assert.Equal("prompt", c.PromptLegacy)
|
assert.Equal("prompt", c.PromptLegacy)
|
||||||
@ -165,6 +175,18 @@ func TestConfigParseIni(t *testing.T) {
|
|||||||
assert.Equal("inicookiename", c.CookieName, "should be read from ini file")
|
assert.Equal("inicookiename", c.CookieName, "should be read from ini file")
|
||||||
assert.Equal("csrfcookiename", c.CSRFCookieName, "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("/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) {
|
func TestConfigFileBackwardsCompatability(t *testing.T) {
|
||||||
@ -182,11 +204,88 @@ func TestConfigParseEnvironment(t *testing.T) {
|
|||||||
assert := assert.New(t)
|
assert := assert.New(t)
|
||||||
os.Setenv("COOKIE_NAME", "env_cookie_name")
|
os.Setenv("COOKIE_NAME", "env_cookie_name")
|
||||||
os.Setenv("PROVIDERS_GOOGLE_CLIENT_ID", "env_client_id")
|
os.Setenv("PROVIDERS_GOOGLE_CLIENT_ID", "env_client_id")
|
||||||
|
os.Setenv("COOKIE_DOMAIN", "test1.com,example.org")
|
||||||
|
os.Setenv("DOMAIN", "test2.com,example.org")
|
||||||
|
os.Setenv("WHITELIST", "test3.com,example.org")
|
||||||
|
|
||||||
c, err := NewConfig([]string{})
|
c, err := NewConfig([]string{})
|
||||||
assert.Nil(err)
|
assert.Nil(err)
|
||||||
|
|
||||||
assert.Equal("env_cookie_name", c.CookieName, "variable should be read from environment")
|
assert.Equal("env_cookie_name", c.CookieName, "variable should be read from environment")
|
||||||
assert.Equal("env_client_id", c.Providers.Google.ClientId, "namespace variable should be read from environment")
|
assert.Equal("env_client_id", c.Providers.Google.ClientID, "namespace variable should be read from environment")
|
||||||
|
assert.Equal([]CookieDomain{
|
||||||
|
*NewCookieDomain("test1.com"),
|
||||||
|
*NewCookieDomain("example.org"),
|
||||||
|
}, c.CookieDomains, "array variable should be read from environment COOKIE_DOMAIN")
|
||||||
|
assert.Equal(CommaSeparatedList{"test2.com", "example.org"}, c.Domains, "array variable should be read from environment DOMAIN")
|
||||||
|
assert.Equal(CommaSeparatedList{"test3.com", "example.org"}, c.Whitelist, "array variable should be read from environment WHITELIST")
|
||||||
|
|
||||||
|
os.Unsetenv("COOKIE_NAME")
|
||||||
|
os.Unsetenv("PROVIDERS_GOOGLE_CLIENT_ID")
|
||||||
|
os.Unsetenv("COOKIE_DOMAIN")
|
||||||
|
os.Unsetenv("DOMAIN")
|
||||||
|
os.Unsetenv("WHITELIST")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigParseEnvironmentBackwardsCompatability(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
vars := map[string]string{
|
||||||
|
"CLIENT_ID": "clientid",
|
||||||
|
"CLIENT_SECRET": "verysecret",
|
||||||
|
"PROMPT": "prompt",
|
||||||
|
"COOKIE_SECRET": "veryverysecret",
|
||||||
|
"LIFETIME": "200",
|
||||||
|
"COOKIE_SECURE": "false",
|
||||||
|
"COOKIE_DOMAINS": "test1.com,example.org",
|
||||||
|
"COOKIE_DOMAIN": "another1.net",
|
||||||
|
"DOMAIN": "test2.com,example.org",
|
||||||
|
"WHITELIST": "test3.com,example.org",
|
||||||
|
}
|
||||||
|
for k, v := range vars {
|
||||||
|
os.Setenv(k, v)
|
||||||
|
}
|
||||||
|
c, err := NewConfig([]string{})
|
||||||
|
require.Nil(t, 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 := CommaSeparatedList{"test2.com", "example.org"}
|
||||||
|
assert.Equal(expected2, c.Domains, "should read legacy comma separated list domains")
|
||||||
|
|
||||||
|
expected3 := CommaSeparatedList{"test3.com", "example.org"}
|
||||||
|
assert.Equal(expected3, c.Whitelist, "should read legacy comma separated list whitelist")
|
||||||
|
|
||||||
|
// Name changed
|
||||||
|
assert.Equal([]byte("veryverysecret"), c.Secret)
|
||||||
|
|
||||||
|
// Google provider params used to be top level
|
||||||
|
assert.Equal("clientid", c.ClientIdLegacy)
|
||||||
|
assert.Equal("clientid", c.Providers.Google.ClientID, "--client-id should set providers.google.client-id")
|
||||||
|
assert.Equal("verysecret", c.ClientSecretLegacy)
|
||||||
|
assert.Equal("verysecret", c.Providers.Google.ClientSecret, "--client-secret should set providers.google.client-secret")
|
||||||
|
assert.Equal("prompt", c.PromptLegacy)
|
||||||
|
assert.Equal("prompt", c.Providers.Google.Prompt, "--prompt should set providers.google.promot")
|
||||||
|
|
||||||
|
// "cookie-secure" used to be a standard go bool flag that could take
|
||||||
|
// true, TRUE, 1, false, FALSE, 0 etc. values.
|
||||||
|
// Here we're checking that format is still supported
|
||||||
|
assert.Equal("false", c.CookieSecureLegacy)
|
||||||
|
assert.True(c.InsecureCookie, "--cookie-secure=false should set insecure-cookie true")
|
||||||
|
|
||||||
|
c, err = NewConfig([]string{"--cookie-secure=TRUE"})
|
||||||
|
assert.Nil(err)
|
||||||
|
assert.Equal("TRUE", c.CookieSecureLegacy)
|
||||||
|
assert.False(c.InsecureCookie, "--cookie-secure=TRUE should set insecure-cookie false")
|
||||||
|
|
||||||
|
for k := range vars {
|
||||||
|
os.Unsetenv(k)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestConfigTransformation(t *testing.T) {
|
func TestConfigTransformation(t *testing.T) {
|
||||||
@ -207,6 +306,97 @@ func TestConfigTransformation(t *testing.T) {
|
|||||||
assert.Equal(time.Second*time.Duration(200), c.Lifetime, "lifetime should be read and converted to duration")
|
assert.Equal(time.Second*time.Duration(200), c.Lifetime, "lifetime should be read and converted to duration")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestConfigValidate(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
// Install new logger + hook
|
||||||
|
var hook *test.Hook
|
||||||
|
log, hook = test.NewNullLogger()
|
||||||
|
log.ExitFunc = func(code int) {}
|
||||||
|
|
||||||
|
// Validate defualt config + rule error
|
||||||
|
c, _ := NewConfig([]string{
|
||||||
|
"--rule.1.action=bad",
|
||||||
|
})
|
||||||
|
c.Validate()
|
||||||
|
|
||||||
|
logs := hook.AllEntries()
|
||||||
|
assert.Len(logs, 3)
|
||||||
|
|
||||||
|
// Should have fatal error requiring secret
|
||||||
|
assert.Equal("\"secret\" option must be set", logs[0].Message)
|
||||||
|
assert.Equal(logrus.FatalLevel, logs[0].Level)
|
||||||
|
|
||||||
|
// Should also have default provider (google) error
|
||||||
|
assert.Equal("providers.google.client-id, providers.google.client-secret must be set", logs[1].Message)
|
||||||
|
assert.Equal(logrus.FatalLevel, logs[1].Level)
|
||||||
|
|
||||||
|
// Should validate rule
|
||||||
|
assert.Equal("invalid rule action, must be \"auth\" or \"allow\"", logs[2].Message)
|
||||||
|
assert.Equal(logrus.FatalLevel, logs[2].Level)
|
||||||
|
|
||||||
|
hook.Reset()
|
||||||
|
|
||||||
|
// Validate with invalid providers
|
||||||
|
c, _ = NewConfig([]string{
|
||||||
|
"--secret=veryverysecret",
|
||||||
|
"--providers.google.client-id=id",
|
||||||
|
"--providers.google.client-secret=secret",
|
||||||
|
"--rule.1.action=auth",
|
||||||
|
"--rule.1.provider=bad2",
|
||||||
|
})
|
||||||
|
c.Validate()
|
||||||
|
|
||||||
|
logs = hook.AllEntries()
|
||||||
|
assert.Len(logs, 1)
|
||||||
|
|
||||||
|
// Should have error for rule provider
|
||||||
|
assert.Equal("Unknown provider: bad2", logs[0].Message)
|
||||||
|
assert.Equal(logrus.FatalLevel, logs[0].Level)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigGetProvider(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
c, _ := NewConfig([]string{})
|
||||||
|
|
||||||
|
// Should be able to get "google" provider
|
||||||
|
p, err := c.GetProvider("google")
|
||||||
|
assert.Nil(err)
|
||||||
|
assert.Equal(&c.Providers.Google, p)
|
||||||
|
|
||||||
|
// Should be able to get "oidc" provider
|
||||||
|
p, err = c.GetProvider("oidc")
|
||||||
|
assert.Nil(err)
|
||||||
|
assert.Equal(&c.Providers.OIDC, p)
|
||||||
|
|
||||||
|
// Should be able to get "generic-oauth" provider
|
||||||
|
p, err = c.GetProvider("generic-oauth")
|
||||||
|
assert.Nil(err)
|
||||||
|
assert.Equal(&c.Providers.GenericOAuth, p)
|
||||||
|
|
||||||
|
// Should catch unknown provider
|
||||||
|
p, err = c.GetProvider("bad")
|
||||||
|
if assert.Error(err) {
|
||||||
|
assert.Equal("Unknown provider: bad", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigGetConfiguredProvider(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
c, _ := NewConfig([]string{})
|
||||||
|
|
||||||
|
// Should be able to get "google" default provider
|
||||||
|
p, err := c.GetConfiguredProvider("google")
|
||||||
|
assert.Nil(err)
|
||||||
|
assert.Equal(&c.Providers.Google, p)
|
||||||
|
|
||||||
|
// Should fail to get valid "oidc" provider as it's not configured
|
||||||
|
p, err = c.GetConfiguredProvider("oidc")
|
||||||
|
if assert.Error(err) {
|
||||||
|
assert.Equal("Unconfigured provider: oidc", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestConfigCommaSeparatedList(t *testing.T) {
|
func TestConfigCommaSeparatedList(t *testing.T) {
|
||||||
assert := assert.New(t)
|
assert := assert.New(t)
|
||||||
list := CommaSeparatedList{}
|
list := CommaSeparatedList{}
|
||||||
|
@ -6,9 +6,10 @@ import (
|
|||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
var log logrus.FieldLogger
|
var log *logrus.Logger
|
||||||
|
|
||||||
func NewDefaultLogger() logrus.FieldLogger {
|
// NewDefaultLogger creates a new logger based on the current configuration
|
||||||
|
func NewDefaultLogger() *logrus.Logger {
|
||||||
// Setup logger
|
// Setup logger
|
||||||
log = logrus.StandardLogger()
|
log = logrus.StandardLogger()
|
||||||
logrus.SetOutput(os.Stdout)
|
logrus.SetOutput(os.Stdout)
|
||||||
|
99
internal/provider/generic_oauth.go
Normal file
99
internal/provider/generic_oauth.go
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
package provider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"golang.org/x/oauth2"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GenericOAuth provider
|
||||||
|
type GenericOAuth struct {
|
||||||
|
AuthURL string `long:"auth-url" env:"AUTH_URL" description:"Auth/Login URL"`
|
||||||
|
TokenURL string `long:"token-url" env:"TOKEN_URL" description:"Token URL"`
|
||||||
|
UserURL string `long:"user-url" env:"USER_URL" description:"URL used to retrieve user info"`
|
||||||
|
ClientID string `long:"client-id" env:"CLIENT_ID" description:"Client ID"`
|
||||||
|
ClientSecret string `long:"client-secret" env:"CLIENT_SECRET" description:"Client Secret" json:"-"`
|
||||||
|
Scopes []string `long:"scope" env:"SCOPE" env-delim:"," default:"profile" default:"email" description:"Scopes"`
|
||||||
|
TokenStyle string `long:"token-style" env:"TOKEN_STYLE" default:"header" choice:"header" choice:"query" description:"How token is presented when querying the User URL"`
|
||||||
|
|
||||||
|
OAuthProvider
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name returns the name of the provider
|
||||||
|
func (o *GenericOAuth) Name() string {
|
||||||
|
return "generic-oauth"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup performs validation and setup
|
||||||
|
func (o *GenericOAuth) Setup(log *logrus.Logger) error {
|
||||||
|
// Check parmas
|
||||||
|
if o.AuthURL == "" || o.TokenURL == "" || o.UserURL == "" || o.ClientID == "" || o.ClientSecret == "" {
|
||||||
|
return errors.New("providers.generic-oauth.auth-url, providers.generic-oauth.token-url, providers.generic-oauth.user-url, providers.generic-oauth.client-id, providers.generic-oauth.client-secret must be set")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create oauth2 config
|
||||||
|
o.Config = &oauth2.Config{
|
||||||
|
ClientID: o.ClientID,
|
||||||
|
ClientSecret: o.ClientSecret,
|
||||||
|
Endpoint: oauth2.Endpoint{
|
||||||
|
AuthURL: o.AuthURL,
|
||||||
|
TokenURL: o.TokenURL,
|
||||||
|
},
|
||||||
|
Scopes: o.Scopes,
|
||||||
|
}
|
||||||
|
|
||||||
|
o.ctx = context.Background()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLoginURL provides the login url for the given redirect uri and state
|
||||||
|
func (o *GenericOAuth) GetLoginURL(redirectURI, state string) string {
|
||||||
|
return o.OAuthGetLoginURL(redirectURI, state)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExchangeCode exchanges the given redirect uri and code for a token
|
||||||
|
func (o *GenericOAuth) ExchangeCode(redirectURI, code string) (string, error) {
|
||||||
|
token, err := o.OAuthExchangeCode(redirectURI, code)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return token.AccessToken, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUser uses the given token and returns a complete provider.User object
|
||||||
|
func (o *GenericOAuth) GetUser(token string) (User, Roles, error) {
|
||||||
|
var user User
|
||||||
|
var roles Roles
|
||||||
|
|
||||||
|
req, err := http.NewRequest("GET", o.UserURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return user, roles, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if o.TokenStyle == "header" {
|
||||||
|
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
|
||||||
|
} else if o.TokenStyle == "query" {
|
||||||
|
q := req.URL.Query()
|
||||||
|
q.Add("access_token", token)
|
||||||
|
req.URL.RawQuery = q.Encode()
|
||||||
|
}
|
||||||
|
|
||||||
|
client := &http.Client{}
|
||||||
|
res, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return user, roles, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer res.Body.Close()
|
||||||
|
err = json.NewDecoder(res.Body).Decode(&user)
|
||||||
|
|
||||||
|
return user, roles, err
|
||||||
|
}
|
140
internal/provider/generic_oauth_test.go
Normal file
140
internal/provider/generic_oauth_test.go
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
package provider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/url"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"golang.org/x/oauth2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Tests
|
||||||
|
|
||||||
|
func TestGenericOAuthName(t *testing.T) {
|
||||||
|
p := GenericOAuth{}
|
||||||
|
assert.Equal(t, "generic-oauth", p.Name())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGenericOAuthSetup(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
p := GenericOAuth{}
|
||||||
|
|
||||||
|
// Check validation
|
||||||
|
err := p.Setup()
|
||||||
|
if assert.Error(err) {
|
||||||
|
assert.Equal("providers.generic-oauth.auth-url, providers.generic-oauth.token-url, providers.generic-oauth.user-url, providers.generic-oauth.client-id, providers.generic-oauth.client-secret must be set", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check setup
|
||||||
|
p = GenericOAuth{
|
||||||
|
AuthURL: "https://provider.com/oauth2/auth",
|
||||||
|
TokenURL: "https://provider.com/oauth2/token",
|
||||||
|
UserURL: "https://provider.com/oauth2/user",
|
||||||
|
ClientID: "id",
|
||||||
|
ClientSecret: "secret",
|
||||||
|
}
|
||||||
|
err = p.Setup()
|
||||||
|
assert.Nil(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGenericOAuthGetLoginURL(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
p := GenericOAuth{
|
||||||
|
AuthURL: "https://provider.com/oauth2/auth",
|
||||||
|
TokenURL: "https://provider.com/oauth2/token",
|
||||||
|
UserURL: "https://provider.com/oauth2/user",
|
||||||
|
ClientID: "idtest",
|
||||||
|
ClientSecret: "secret",
|
||||||
|
Scopes: []string{"scopetest"},
|
||||||
|
}
|
||||||
|
err := p.Setup()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check url
|
||||||
|
uri, err := url.Parse(p.GetLoginURL("http://example.com/_oauth", "state"))
|
||||||
|
assert.Nil(err)
|
||||||
|
assert.Equal("https", uri.Scheme)
|
||||||
|
assert.Equal("provider.com", uri.Host)
|
||||||
|
assert.Equal("/oauth2/auth", uri.Path)
|
||||||
|
|
||||||
|
// Check query string
|
||||||
|
qs := uri.Query()
|
||||||
|
expectedQs := url.Values{
|
||||||
|
"client_id": []string{"idtest"},
|
||||||
|
"redirect_uri": []string{"http://example.com/_oauth"},
|
||||||
|
"response_type": []string{"code"},
|
||||||
|
"scope": []string{"scopetest"},
|
||||||
|
"state": []string{"state"},
|
||||||
|
}
|
||||||
|
assert.Equal(expectedQs, qs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGenericOAuthExchangeCode(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
// Setup server
|
||||||
|
expected := url.Values{
|
||||||
|
"client_id": []string{"idtest"},
|
||||||
|
"client_secret": []string{"sectest"},
|
||||||
|
"code": []string{"code"},
|
||||||
|
"grant_type": []string{"authorization_code"},
|
||||||
|
"redirect_uri": []string{"http://example.com/_oauth"},
|
||||||
|
}
|
||||||
|
server, serverURL := NewOAuthServer(t, map[string]string{
|
||||||
|
"token": expected.Encode(),
|
||||||
|
})
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
// Setup provider
|
||||||
|
p := GenericOAuth{
|
||||||
|
AuthURL: "https://provider.com/oauth2/auth",
|
||||||
|
TokenURL: serverURL.String() + "/token",
|
||||||
|
UserURL: "https://provider.com/oauth2/user",
|
||||||
|
ClientID: "idtest",
|
||||||
|
ClientSecret: "sectest",
|
||||||
|
}
|
||||||
|
err := p.Setup()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// We force AuthStyleInParams to prevent the test failure when the
|
||||||
|
// AuthStyleInHeader is attempted
|
||||||
|
p.Config.Endpoint.AuthStyle = oauth2.AuthStyleInParams
|
||||||
|
|
||||||
|
token, err := p.ExchangeCode("http://example.com/_oauth", "code")
|
||||||
|
assert.Nil(err)
|
||||||
|
assert.Equal("123456789", token)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGenericOAuthGetUser(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
// Setup server
|
||||||
|
server, serverURL := NewOAuthServer(t, nil)
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
// Setup provider
|
||||||
|
p := GenericOAuth{
|
||||||
|
AuthURL: "https://provider.com/oauth2/auth",
|
||||||
|
TokenURL: "https://provider.com/oauth2/token",
|
||||||
|
UserURL: serverURL.String() + "/userinfo",
|
||||||
|
ClientID: "idtest",
|
||||||
|
ClientSecret: "sectest",
|
||||||
|
}
|
||||||
|
err := p.Setup()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// We force AuthStyleInParams to prevent the test failure when the
|
||||||
|
// AuthStyleInHeader is attempted
|
||||||
|
p.Config.Endpoint.AuthStyle = oauth2.AuthStyleInParams
|
||||||
|
|
||||||
|
user, err := p.GetUser("123456789")
|
||||||
|
assert.Nil(err)
|
||||||
|
|
||||||
|
assert.Equal("example@example.com", user.Email)
|
||||||
|
}
|
@ -2,31 +2,68 @@ package provider
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Google provider
|
||||||
type Google struct {
|
type Google struct {
|
||||||
ClientId string `long:"client-id" env:"CLIENT_ID" description:"Client ID"`
|
ClientID string `long:"client-id" env:"CLIENT_ID" description:"Client ID"`
|
||||||
ClientSecret string `long:"client-secret" env:"CLIENT_SECRET" description:"Client Secret" json:"-"`
|
ClientSecret string `long:"client-secret" env:"CLIENT_SECRET" description:"Client Secret" json:"-"`
|
||||||
Scope string
|
Scope string
|
||||||
Prompt string `long:"prompt" env:"PROMPT" description:"Space separated list of OpenID prompt options"`
|
Prompt string `long:"prompt" env:"PROMPT" default:"select_account" description:"Space separated list of OpenID prompt options"`
|
||||||
|
|
||||||
LoginURL *url.URL
|
LoginURL *url.URL
|
||||||
TokenURL *url.URL
|
TokenURL *url.URL
|
||||||
UserURL *url.URL
|
UserURL *url.URL
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *Google) GetLoginURL(redirectUri, state string) string {
|
// Name returns the name of the provider
|
||||||
|
func (g *Google) Name() string {
|
||||||
|
return "google"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup performs validation and setup
|
||||||
|
func (g *Google) Setup(log *logrus.Logger) error {
|
||||||
|
if g.ClientID == "" || g.ClientSecret == "" {
|
||||||
|
return errors.New("providers.google.client-id, providers.google.client-secret must be set")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set static values
|
||||||
|
g.Scope = "https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email"
|
||||||
|
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",
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLoginURL provides the login url for the given redirect uri and state
|
||||||
|
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)
|
||||||
q.Set("response_type", "code")
|
q.Set("response_type", "code")
|
||||||
q.Set("scope", g.Scope)
|
q.Set("scope", g.Scope)
|
||||||
if g.Prompt != "" {
|
if g.Prompt != "" {
|
||||||
q.Set("prompt", g.Prompt)
|
q.Set("prompt", g.Prompt)
|
||||||
}
|
}
|
||||||
q.Set("redirect_uri", redirectUri)
|
q.Set("redirect_uri", redirectURI)
|
||||||
q.Set("state", state)
|
q.Set("state", state)
|
||||||
|
|
||||||
var u url.URL
|
var u url.URL
|
||||||
@ -36,12 +73,13 @@ func (g *Google) GetLoginURL(redirectUri, state string) string {
|
|||||||
return u.String()
|
return u.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *Google) ExchangeCode(redirectUri, code string) (string, error) {
|
// ExchangeCode exchanges the given redirect uri and code for a token
|
||||||
|
func (g *Google) ExchangeCode(redirectURI, code string) (string, error) {
|
||||||
form := url.Values{}
|
form := url.Values{}
|
||||||
form.Set("client_id", g.ClientId)
|
form.Set("client_id", g.ClientID)
|
||||||
form.Set("client_secret", g.ClientSecret)
|
form.Set("client_secret", g.ClientSecret)
|
||||||
form.Set("grant_type", "authorization_code")
|
form.Set("grant_type", "authorization_code")
|
||||||
form.Set("redirect_uri", redirectUri)
|
form.Set("redirect_uri", redirectURI)
|
||||||
form.Set("code", code)
|
form.Set("code", code)
|
||||||
|
|
||||||
res, err := http.PostForm(g.TokenURL.String(), form)
|
res, err := http.PostForm(g.TokenURL.String(), form)
|
||||||
@ -49,30 +87,32 @@ func (g *Google) ExchangeCode(redirectUri, code string) (string, error) {
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
var token Token
|
var token token
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
err = json.NewDecoder(res.Body).Decode(&token)
|
err = json.NewDecoder(res.Body).Decode(&token)
|
||||||
|
|
||||||
return token.Token, err
|
return token.Token, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *Google) GetUser(token string) (User, error) {
|
// GetUser uses the given token and returns a complete provider.User object
|
||||||
|
func (g *Google) GetUser(token string) (User, Roles, error) {
|
||||||
var user User
|
var user User
|
||||||
|
var roles Roles
|
||||||
|
|
||||||
client := &http.Client{}
|
client := &http.Client{}
|
||||||
req, err := http.NewRequest("GET", g.UserURL.String(), nil)
|
req, err := http.NewRequest("GET", g.UserURL.String(), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return user, err
|
return user, roles, err
|
||||||
}
|
}
|
||||||
|
|
||||||
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
|
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
|
||||||
res, err := client.Do(req)
|
res, err := client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return user, err
|
return user, roles, err
|
||||||
}
|
}
|
||||||
|
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
err = json.NewDecoder(res.Body).Decode(&user)
|
err = json.NewDecoder(res.Body).Decode(&user)
|
||||||
|
|
||||||
return user, err
|
return user, roles, err
|
||||||
}
|
}
|
||||||
|
148
internal/provider/google_test.go
Normal file
148
internal/provider/google_test.go
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
package provider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/url"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Tests
|
||||||
|
|
||||||
|
func TestGoogleName(t *testing.T) {
|
||||||
|
p := Google{}
|
||||||
|
assert.Equal(t, "google", p.Name())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGoogleSetup(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
p := Google{}
|
||||||
|
|
||||||
|
// Check validation
|
||||||
|
err := p.Setup()
|
||||||
|
if assert.Error(err) {
|
||||||
|
assert.Equal("providers.google.client-id, providers.google.client-secret must be set", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check setup
|
||||||
|
p = Google{
|
||||||
|
ClientID: "id",
|
||||||
|
ClientSecret: "secret",
|
||||||
|
}
|
||||||
|
err = p.Setup()
|
||||||
|
assert.Nil(err)
|
||||||
|
assert.Equal("https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email", p.Scope)
|
||||||
|
assert.Equal("", p.Prompt)
|
||||||
|
|
||||||
|
assert.Equal(&url.URL{
|
||||||
|
Scheme: "https",
|
||||||
|
Host: "accounts.google.com",
|
||||||
|
Path: "/o/oauth2/auth",
|
||||||
|
}, p.LoginURL)
|
||||||
|
|
||||||
|
assert.Equal(&url.URL{
|
||||||
|
Scheme: "https",
|
||||||
|
Host: "www.googleapis.com",
|
||||||
|
Path: "/oauth2/v3/token",
|
||||||
|
}, p.TokenURL)
|
||||||
|
|
||||||
|
assert.Equal(&url.URL{
|
||||||
|
Scheme: "https",
|
||||||
|
Host: "www.googleapis.com",
|
||||||
|
Path: "/oauth2/v2/userinfo",
|
||||||
|
}, p.UserURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGoogleGetLoginURL(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
p := Google{
|
||||||
|
ClientID: "idtest",
|
||||||
|
ClientSecret: "sectest",
|
||||||
|
Scope: "scopetest",
|
||||||
|
Prompt: "consent select_account",
|
||||||
|
LoginURL: &url.URL{
|
||||||
|
Scheme: "https",
|
||||||
|
Host: "google.com",
|
||||||
|
Path: "/auth",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check url
|
||||||
|
uri, err := url.Parse(p.GetLoginURL("http://example.com/_oauth", "state"))
|
||||||
|
assert.Nil(err)
|
||||||
|
assert.Equal("https", uri.Scheme)
|
||||||
|
assert.Equal("google.com", uri.Host)
|
||||||
|
assert.Equal("/auth", uri.Path)
|
||||||
|
|
||||||
|
// Check query string
|
||||||
|
qs := uri.Query()
|
||||||
|
expectedQs := url.Values{
|
||||||
|
"client_id": []string{"idtest"},
|
||||||
|
"redirect_uri": []string{"http://example.com/_oauth"},
|
||||||
|
"response_type": []string{"code"},
|
||||||
|
"scope": []string{"scopetest"},
|
||||||
|
"prompt": []string{"consent select_account"},
|
||||||
|
"state": []string{"state"},
|
||||||
|
}
|
||||||
|
assert.Equal(expectedQs, qs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGoogleExchangeCode(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
// Setup server
|
||||||
|
expected := url.Values{
|
||||||
|
"client_id": []string{"idtest"},
|
||||||
|
"client_secret": []string{"sectest"},
|
||||||
|
"code": []string{"code"},
|
||||||
|
"grant_type": []string{"authorization_code"},
|
||||||
|
"redirect_uri": []string{"http://example.com/_oauth"},
|
||||||
|
}
|
||||||
|
server, serverURL := NewOAuthServer(t, map[string]string{
|
||||||
|
"token": expected.Encode(),
|
||||||
|
})
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
// Setup provider
|
||||||
|
p := Google{
|
||||||
|
ClientID: "idtest",
|
||||||
|
ClientSecret: "sectest",
|
||||||
|
Scope: "scopetest",
|
||||||
|
Prompt: "consent select_account",
|
||||||
|
TokenURL: &url.URL{
|
||||||
|
Scheme: serverURL.Scheme,
|
||||||
|
Host: serverURL.Host,
|
||||||
|
Path: "/token",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
token, err := p.ExchangeCode("http://example.com/_oauth", "code")
|
||||||
|
assert.Nil(err)
|
||||||
|
assert.Equal("123456789", token)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGoogleGetUser(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
// Setup server
|
||||||
|
server, serverURL := NewOAuthServer(t, nil)
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
// Setup provider
|
||||||
|
p := Google{
|
||||||
|
ClientID: "idtest",
|
||||||
|
ClientSecret: "sectest",
|
||||||
|
Scope: "scopetest",
|
||||||
|
Prompt: "consent select_account",
|
||||||
|
UserURL: &url.URL{
|
||||||
|
Scheme: serverURL.Scheme,
|
||||||
|
Host: serverURL.Host,
|
||||||
|
Path: "/userinfo",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := p.GetUser("123456789")
|
||||||
|
assert.Nil(err)
|
||||||
|
|
||||||
|
assert.Equal("example@example.com", user.Email)
|
||||||
|
}
|
113
internal/provider/oidc.go
Normal file
113
internal/provider/oidc.go
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
package provider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/coreos/go-oidc"
|
||||||
|
"golang.org/x/oauth2"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
// OIDC provider
|
||||||
|
type OIDC struct {
|
||||||
|
IssuerURL string `long:"issuer-url" env:"ISSUER_URL" description:"Issuer URL"`
|
||||||
|
ClientID string `long:"client-id" env:"CLIENT_ID" description:"Client ID"`
|
||||||
|
ClientSecret string `long:"client-secret" env:"CLIENT_SECRET" description:"Client Secret" json:"-"`
|
||||||
|
|
||||||
|
OAuthProvider
|
||||||
|
|
||||||
|
provider *oidc.Provider
|
||||||
|
verifier *oidc.IDTokenVerifier
|
||||||
|
|
||||||
|
log *logrus.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name returns the name of the provider
|
||||||
|
func (o *OIDC) Name() string {
|
||||||
|
return "oidc"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup performs validation and setup
|
||||||
|
func (o *OIDC) Setup(log *logrus.Logger) error {
|
||||||
|
o.log = log
|
||||||
|
|
||||||
|
// Check parms
|
||||||
|
if o.IssuerURL == "" || o.ClientID == "" || o.ClientSecret == "" {
|
||||||
|
return errors.New("providers.oidc.issuer-url, providers.oidc.client-id, providers.oidc.client-secret must be set")
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
o.ctx = context.Background()
|
||||||
|
|
||||||
|
// Try to initiate provider
|
||||||
|
o.provider, err = oidc.NewProvider(o.ctx, o.IssuerURL)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create oauth2 config
|
||||||
|
o.Config = &oauth2.Config{
|
||||||
|
ClientID: o.ClientID,
|
||||||
|
ClientSecret: o.ClientSecret,
|
||||||
|
Endpoint: o.provider.Endpoint(),
|
||||||
|
|
||||||
|
// "openid" is a required scope for OpenID Connect flows.
|
||||||
|
Scopes: []string{oidc.ScopeOpenID, "profile", "email"},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create OIDC verifier
|
||||||
|
o.verifier = o.provider.Verifier(&oidc.Config{
|
||||||
|
ClientID: o.ClientID,
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLoginURL provides the login url for the given redirect uri and state
|
||||||
|
func (o *OIDC) GetLoginURL(redirectURI, state string) string {
|
||||||
|
return o.OAuthGetLoginURL(redirectURI, state)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExchangeCode exchanges the given redirect uri and code for a token
|
||||||
|
func (o *OIDC) ExchangeCode(redirectURI, code string) (string, error) {
|
||||||
|
token, err := o.OAuthExchangeCode(redirectURI, code)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
o.log.WithField("accessToken", token.AccessToken).Debug("getUser")
|
||||||
|
|
||||||
|
// Extract ID token
|
||||||
|
rawIDToken, ok := token.Extra("id_token").(string)
|
||||||
|
if !ok {
|
||||||
|
return "", errors.New("Missing id_token")
|
||||||
|
}
|
||||||
|
|
||||||
|
return rawIDToken, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUser uses the given token and returns a complete provider.User object
|
||||||
|
func (o *OIDC) GetUser(token string) (User, Roles, error) {
|
||||||
|
var user User
|
||||||
|
var roles Roles
|
||||||
|
|
||||||
|
// Parse & Verify ID Token
|
||||||
|
idToken, err := o.verifier.Verify(o.ctx, token)
|
||||||
|
if err != nil {
|
||||||
|
return user, roles, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract custom claims
|
||||||
|
if err := idToken.Claims(&user); err != nil {
|
||||||
|
return user, roles, err
|
||||||
|
}
|
||||||
|
o.log.WithField("user", user).Debug("getUser")
|
||||||
|
|
||||||
|
if err := idToken.Claims(&roles); err != nil {
|
||||||
|
return user, roles, err
|
||||||
|
}
|
||||||
|
o.log.WithField("roles", roles).Debug("getUser")
|
||||||
|
|
||||||
|
return user, roles, nil
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user