16 Commits

Author SHA1 Message Date
e41e8e1a17 ignore ci script for docker 2023-11-07 09:10:16 +01:00
86894f72ed add ci script 2023-11-07 09:08:20 +01:00
3f9d70e87b tidied up 2023-11-07 08:54:40 +01:00
fb24320552 role added to configuration 2023-11-06 22:25:00 +01:00
f6120640d2 evaluate role in higher layer 2023-11-06 22:09:29 +01:00
ab2d527dbd we get closer 2023-11-06 19:59:31 +01:00
5828a9a5a2 debugging for analyzing token 2023-11-06 18:15:03 +01:00
c4317b7503 Allow to be run without middleware + improve request reading consistency (#217)
Prior to this change, the request URI was only ever read from the
X-Forwarded-Uri header which was only set when the container was
accessed via the forwardauth middleware. As such, it was necessary
to apply the treafik-forward-auth middleware to the treafik-forward-auth
container when running auth host mode.
This is a quirk, unnecessary complexity and is a frequent source of
configuration issues.
2021-06-24 21:45:28 +01:00
4ffb6593d5 Add GitHub Actions workflow for creating binaries for releases (#184) (#199)
* Add GitHub Actions workflow for creating binaries for releases
* Add sentence about binary files to README
* Cleanup + nicer way querying GitHub API
2021-02-01 20:28:00 +00:00
6c6f75e80d Make listen port configurable (#230)
Co-authored-by: Tobias Hess <tobias.hess@energiekoppler.com>
2021-02-01 20:10:50 +00:00
8be8244b13 Switch to Github Actions for CI (#219) 2021-01-03 13:44:40 +00:00
f96a3fb332 Remove double brackets typo in readme (#218) 2020-12-10 21:39:49 +00:00
c19f622fbd Create codeql-analysis.yml 2020-10-01 09:29:36 +01:00
04f5499f0b Allow override of domains and whitelist in rules (#169)
Co-authored-by: Mathieu Cantin <mcantin@petalmd.com>
Co-authored-by: Pete Shaw <lozlow@users.noreply.github.com>
2020-09-23 14:50:15 +01:00
41560feaa7 Support concurrent CSRF cookies by using a prefix of nonce (#187)
* Support concurrent CSRF cookies by using a prefix of nonce.
* Move ValidateState out and make CSRF cookies last 1h
* add tests to check csrf cookie nam + minor tweaks

Co-authored-by: Michal Witkowski <michal@cerberus>
2020-09-23 14:48:04 +01:00
1743537438 Fix simple-separate-pod url path (#148)
There is a missing slash in the `kubernetes/simple-separate-pod` example link, leading to a 404. This change fixes that url in the README.md file.
2020-07-17 14:14:27 +01:00
21 changed files with 398 additions and 234 deletions

View File

@ -1,3 +1,4 @@
example example
.travis.yml .travis.yml
.git .git
.gitlab-ci.yml

2
.github/FUNDING.yml vendored
View File

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

6
.gitlab-ci.yml Normal file
View File

@ -0,0 +1,6 @@
include:
- project: dockerized/commons
ref: master
file: gitlab-ci-template.yml

View File

@ -1,5 +0,0 @@
language: go
sudo: false
go:
- "1.12"
script: env GO111MODULE=on go test -v ./...

View File

@ -1,18 +0,0 @@
FROM golang:1.13-alpine as builder
# Setup
RUN mkdir -p /go/src/github.com/thomseddon/traefik-forward-auth
WORKDIR /go/src/github.com/thomseddon/traefik-forward-auth
# Add libraries
RUN apk add --no-cache git
# Copy & build
ADD . /go/src/github.com/thomseddon/traefik-forward-auth/
RUN CGO_ENABLED=0 GOOS=linux GOARCH=arm GO111MODULE=on go build -a -installsuffix nocgo -o /traefik-forward-auth github.com/thomseddon/traefik-forward-auth/cmd
# Copy into scratch container
FROM scratch
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /traefik-forward-auth ./
ENTRYPOINT ["./traefik-forward-auth"]

View File

@ -1,18 +0,0 @@
FROM golang:1.13-alpine as builder
# Setup
RUN mkdir -p /go/src/github.com/thomseddon/traefik-forward-auth
WORKDIR /go/src/github.com/thomseddon/traefik-forward-auth
# Add libraries
RUN apk add --no-cache git
# Copy & build
ADD . /go/src/github.com/thomseddon/traefik-forward-auth/
RUN CGO_ENABLED=0 GOOS=linux GOARCH=arm64 GO111MODULE=on go build -a -installsuffix nocgo -o /traefik-forward-auth github.com/thomseddon/traefik-forward-auth/cmd
# Copy into scratch container
FROM scratch
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /traefik-forward-auth ./
ENTRYPOINT ["./traefik-forward-auth"]

View File

@ -1,5 +1,6 @@
MIT License MIT License
Copyright (c) [2023] [Wolfgang Hottgenroth]
Copyright (c) [2018] [Thom Seddon] Copyright (c) [2018] [Thom Seddon]
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy

View File

@ -1,5 +1,5 @@
# Traefik Forward Auth [![Build Status](https://travis-ci.org/thomseddon/traefik-forward-auth.svg?branch=master)](https://travis-ci.org/thomseddon/traefik-forward-auth) [![Go Report Card](https://goreportcard.com/badge/github.com/thomseddon/traefik-forward-auth)](https://goreportcard.com/report/github.com/thomseddon/traefik-forward-auth) ![Docker Pulls](https://img.shields.io/docker/pulls/thomseddon/traefik-forward-auth.svg) [![GitHub release](https://img.shields.io/github/release/thomseddon/traefik-forward-auth.svg)](https://GitHub.com/thomseddon/traefik-forward-auth/releases/) # Traefik Forward Auth ![Build Status](https://img.shields.io/github/workflow/status/thomseddon/traefik-forward-auth/CI) [![Go Report Card](https://goreportcard.com/badge/github.com/thomseddon/traefik-forward-auth)](https://goreportcard.com/report/github.com/thomseddon/traefik-forward-auth) ![Docker Pulls](https://img.shields.io/docker/pulls/thomseddon/traefik-forward-auth.svg) [![GitHub release](https://img.shields.io/github/release/thomseddon/traefik-forward-auth.svg)](https://GitHub.com/thomseddon/traefik-forward-auth/releases/)
A minimal forward authentication service that provides OAuth/SSO login and authentication for the [traefik](https://github.com/containous/traefik) reverse proxy/load balancer. A minimal forward authentication service that provides OAuth/SSO login and authentication for the [traefik](https://github.com/containous/traefik) reverse proxy/load balancer.
@ -9,8 +9,8 @@ A minimal forward authentication service that provides OAuth/SSO login and authe
- Seamlessly overlays any http service with a single endpoint (see: `url-path` in [Configuration](#configuration)) - Seamlessly overlays any http service with a single endpoint (see: `url-path` in [Configuration](#configuration))
- Supports multiple providers including Google and OpenID Connect (supported by Azure, Github, Salesforce etc.) - Supports multiple providers including Google and OpenID Connect (supported by Azure, Github, Salesforce etc.)
- Supports multiple domains/subdomains by dynamically generating redirect_uri's - Supports multiple domains/subdomains by dynamically generating redirect_uri's
- Allows authentication to be selectively applied/bypassed based on request parameters (see `rules` in [Configuration](#configuration))) - Allows authentication to be selectively applied/bypassed based on request parameters (see `rules` in [Configuration](#configuration))
- Supports use of centralised authentication host/redirect_uri (see `auth-host` in [Configuration](#configuration))) - Supports use of centralised authentication host/redirect_uri (see `auth-host` in [Configuration](#configuration))
- Allows authentication to persist across multiple domains (see [Cookie Domains](#cookie-domains)) - Allows authentication to persist across multiple domains (see [Cookie Domains](#cookie-domains))
- Supports extended authentication beyond Google token lifetime (see: `lifetime` in [Configuration](#configuration)) - Supports extended authentication beyond Google token lifetime (see: `lifetime` in [Configuration](#configuration))
@ -47,6 +47,8 @@ You can also use the latest incremental releases found on [docker hub](https://h
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`). ARM releases are also available on docker hub, just append `-arm` or `-arm64` to your desired released (e.g. `2-arm` or `2.1-arm64`).
We also build binary files for usage without docker starting with releases after 2.2.0 You can find these as assets of the specific GitHub release.
#### Upgrade Guide #### Upgrade Guide
v2 was released in June 2019, whilst this is fully backwards compatible, a number of configuration options were modified, please see the [upgrade guide](https://github.com/thomseddon/traefik-forward-auth/wiki/v2-Upgrade-Guide) to prevent warnings on startup and ensure you are using the current configuration. v2 was released in June 2019, whilst this is fully backwards compatible, a number of configuration options were modified, please see the [upgrade guide](https://github.com/thomseddon/traefik-forward-auth/wiki/v2-Upgrade-Guide) to prevent warnings on startup and ensure you are using the current configuration.
@ -92,7 +94,7 @@ services:
#### Advanced: #### 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/masterexamples/traefik-v2/kubernetes/simple-separate-pod/). 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. 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.
@ -162,6 +164,7 @@ Application Options:
--url-path= Callback URL Path (default: /_oauth) [$URL_PATH] --url-path= Callback URL Path (default: /_oauth) [$URL_PATH]
--secret= Secret used for signing (required) [$SECRET] --secret= Secret used for signing (required) [$SECRET]
--whitelist= Only allow given email addresses, can be set multiple times [$WHITELIST] --whitelist= Only allow given email addresses, can be set multiple times [$WHITELIST]
--port= Port to listen on (default: 4181) [$PORT]
--rule.<name>.<param>= Rule definitions, param can be: "action", "rule" or "provider" --rule.<name>.<param>= Rule definitions, param can be: "action", "rule" or "provider"
Google Provider: Google Provider:
@ -321,6 +324,7 @@ All options can be supplied in any of the following ways, in the following prece
- `action` - same usage as [`default-action`](#default-action), supported values: - `action` - same usage as [`default-action`](#default-action), supported values:
- `auth` (default) - `auth` (default)
- `allow` - `allow`
- `domains` - optional, same usage as [`domain`](#domain)
- `provider` - same usage as [`default-provider`](#default-provider), supported values: - `provider` - same usage as [`default-provider`](#default-provider), supported values:
- `google` - `google`
- `oidc` - `oidc`
@ -333,6 +337,7 @@ All options can be supplied in any of the following ways, in the following prece
- ``Path(`path`, `/articles/{category}/{id:[0-9]+}`, ...)`` - ``Path(`path`, `/articles/{category}/{id:[0-9]+}`, ...)``
- ``PathPrefix(`/products/`, `/articles/{category}/{id:[0-9]+}`)`` - ``PathPrefix(`/products/`, `/articles/{category}/{id:[0-9]+}`)``
- ``Query(`foo=bar`, `bar=baz`)`` - ``Query(`foo=bar`, `bar=baz`)``
- `whitelist` - optional, same usage as whitelist`](#whitelist)
For example: For example:
``` ```
@ -348,6 +353,11 @@ All options can be supplied in any of the following ways, in the following prece
rule.oidc.action = auth rule.oidc.action = auth
rule.oidc.provider = oidc rule.oidc.provider = oidc
rule.oidc.rule = PathPrefix(`/github`) rule.oidc.rule = PathPrefix(`/github`)
# Allow jane@example.com to `/janes-eyes-only`
rule.two.action = allow
rule.two.rule = Path(`/janes-eyes-only`)
rule.two.whitelist = jane@example.com
``` ```
Note: It is possible to break your redirect flow with rules, please be careful not to create an `allow` rule that matches your redirect_uri unless you know what you're doing. This limitation is being tracked in in #101 and the behaviour will change in future releases. 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.
@ -361,7 +371,7 @@ You can restrict who can login with the following parameters:
* `domain` - Use this to limit logins to a specific domain, e.g. test.com only * `domain` - Use this to limit logins to a specific domain, e.g. test.com only
* `whitelist` - Use this to only allow specific users to login e.g. thom@test.com only * `whitelist` - Use this to only allow specific users to login e.g. thom@test.com only
Note, if you pass 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). Note, if you pass both `whitelist` and `domain`, then the default behaviour is for only `whitelist` to be used and `domain` will be effectively ignored. You can allow users matching *either* `whitelist` or `domain` by passing the `match-whitelist-or-domain` parameter (this will be the default behaviour in v3). If you set `domains` or `whitelist` on a rule, the global configuration is ignored.
### Forwarded Headers ### Forwarded Headers
@ -416,8 +426,6 @@ spec:
- name: traefik-forward-auth - name: traefik-forward-auth
``` ```
Note: If using auth host mode, you must apply the middleware to your auth host ingress.
See the examples directory for more examples. See the examples directory for more examples.
#### Selective Container Authentication in Swarm #### Selective Container Authentication in Swarm
@ -432,8 +440,6 @@ whoami:
- "traefik.http.routers.whoami.middlewares=traefik-forward-auth" - "traefik.http.routers.whoami.middlewares=traefik-forward-auth"
``` ```
Note: If using auth host mode, you must apply the middleware to the traefik-forward-auth container.
See the examples directory for more examples. See the examples directory for more examples.
#### Rules Based Authentication #### Rules Based Authentication

View File

@ -1,6 +1,7 @@
package main package main
import ( import (
"fmt"
"net/http" "net/http"
internal "github.com/thomseddon/traefik-forward-auth/internal" internal "github.com/thomseddon/traefik-forward-auth/internal"
@ -24,7 +25,8 @@ func main() {
http.HandleFunc("/", server.RootHandler) http.HandleFunc("/", server.RootHandler)
// Start // Start
log.Info("wn test01 variant")
log.WithField("config", config).Debug("Starting with config") log.WithField("config", config).Debug("Starting with config")
log.Info("Listening on :4181") log.Infof("Listening on :%d", config.Port)
log.Info(http.ListenAndServe(":4181", nil)) log.Info(http.ListenAndServe(fmt.Sprintf(":%d", config.Port), nil))
} }

View File

@ -16,7 +16,5 @@ spec:
services: services:
- name: traefik-forward-auth - name: traefik-forward-auth
port: 4181 port: 4181
middlewares:
- name: traefik-forward-auth
tls: tls:
certresolver: default certresolver: default

15
go.sum
View File

@ -127,7 +127,6 @@ github.com/go-acme/lego/v3 v3.2.0/go.mod h1:074uqt+JS6plx+c9Xaiz6+L+GBb+7itGtzfc
github.com/go-cmd/cmd v1.0.5/go.mod h1:y8q8qlK5wQibcw63djSl/ntiHUHXHGdCkPk0j4QeW4s= github.com/go-cmd/cmd v1.0.5/go.mod h1:y8q8qlK5wQibcw63djSl/ntiHUHXHGdCkPk0j4QeW4s=
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
github.com/go-ini/ini v1.44.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= github.com/go-ini/ini v1.44.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
github.com/go-kit/kit v0.8.0 h1:Wz+5lgoB0kkuqLEc6NVmwRknTKP6dTGbSqvhZtBI/j0=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0 h1:wDJmvq38kDhkVxi50ni9ykkdUr1PKgqKOoi01fa0Mdk= github.com/go-kit/kit v0.9.0 h1:wDJmvq38kDhkVxi50ni9ykkdUr1PKgqKOoi01fa0Mdk=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
@ -145,7 +144,6 @@ github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4er
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
@ -229,7 +227,6 @@ github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfV
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kolo/xmlrpc v0.0.0-20190717152603-07c4ee3fd181/go.mod h1:o03bZfuBwAXHetKXuInt4S7omeXUu62/A845kiycsSQ= github.com/kolo/xmlrpc v0.0.0-20190717152603-07c4ee3fd181/go.mod h1:o03bZfuBwAXHetKXuInt4S7omeXUu62/A845kiycsSQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s= github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
@ -349,7 +346,6 @@ github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdh
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.1 h1:GL2rEmy6nsikmW0r8opw9JIRScdMF5hA8cOYLH7In1k=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
@ -357,13 +353,11 @@ github.com/skratchdot/open-golang v0.0.0-20160302144031-75fb7ed4208c/go.mod h1:s
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/spf13/pflag v1.0.1 h1:aCvUg6QPl3ibpQUxyLkrEkCHtPqYJL4x9AuhqVqFis4=
github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
@ -436,7 +430,6 @@ golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73r
golang.org/x/net v0.0.0-20190206173232-65e2d4e15006/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190206173232-65e2d4e15006/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@ -457,7 +450,6 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sys v0.0.0-20180622082034-63fc586f45fe/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180622082034-63fc586f45fe/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33 h1:I6FyU15t786LL7oL/hn43zqTuEGr4PN7F4XJ1p4E3Y8=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -469,7 +461,6 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -493,10 +484,8 @@ golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135 h1:5Beo0mZN8dRzgrMMkDp0jc8YXQKx9DiJ2k1dkvGsn5A=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.0.0-20190331200053-3d26580ed485 h1:OB/uP/Puiu5vS5QMRPrXCDWUPb+kt8f1KW8oQzFejQw=
gonum.org/v1/gonum v0.0.0-20190331200053-3d26580ed485/go.mod h1:2ltnJ7xHfj0zHS40VVPYEAAMTa3ZGguvHGBSJeRWqE0= gonum.org/v1/gonum v0.0.0-20190331200053-3d26580ed485/go.mod h1:2ltnJ7xHfj0zHS40VVPYEAAMTa3ZGguvHGBSJeRWqE0=
gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=
gonum.org/v1/netlib v0.0.0-20190331212654-76723241ea4e/go.mod h1:kS+toOQn6AQKjmKJ7gzohV1XkqsFehRA2FbsbkopSuQ= gonum.org/v1/netlib v0.0.0-20190331212654-76723241ea4e/go.mod h1:kS+toOQn6AQKjmKJ7gzohV1XkqsFehRA2FbsbkopSuQ=
@ -504,7 +493,6 @@ google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMt
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0 h1:KxkO13IPW4Lslp2bz+KHP2E3gtFlrIGNThxkZQ3g+4c= google.golang.org/appengine v1.5.0 h1:KxkO13IPW4Lslp2bz+KHP2E3gtFlrIGNThxkZQ3g+4c=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
@ -553,11 +541,8 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
k8s.io/api v0.0.0-20190718183219-b59d8169aab5/go.mod h1:TBhBqb1AWbBQbW3XRusr7n7E4v2+5ZY8r8sAMnyFC5A= k8s.io/api v0.0.0-20190718183219-b59d8169aab5/go.mod h1:TBhBqb1AWbBQbW3XRusr7n7E4v2+5ZY8r8sAMnyFC5A=
k8s.io/apimachinery v0.0.0-20190612205821-1799e75a0719/go.mod h1:I4A+glKBHiTgiEjQiCCQfCAIcIMFGt291SmsvcrFzJA= k8s.io/apimachinery v0.0.0-20190612205821-1799e75a0719/go.mod h1:I4A+glKBHiTgiEjQiCCQfCAIcIMFGt291SmsvcrFzJA=
k8s.io/client-go v0.0.0-20190718183610-8e956561bbf5/go.mod h1:ozblAqkW495yoAX60QZyxQBq5W0YixE9Ffn4F91RO0g= k8s.io/client-go v0.0.0-20190718183610-8e956561bbf5/go.mod h1:ozblAqkW495yoAX60QZyxQBq5W0YixE9Ffn4F91RO0g=
k8s.io/code-generator v0.0.0-20190612205613-18da4a14b22b h1:p+PRuwXWwk5e+UYvicGiavEupapqM5NOxUl3y1GkD6c=
k8s.io/code-generator v0.0.0-20190612205613-18da4a14b22b/go.mod h1:G8bQwmHm2eafm5bgtX67XDZQ8CWKSGu9DekI+yN4Y5I= k8s.io/code-generator v0.0.0-20190612205613-18da4a14b22b/go.mod h1:G8bQwmHm2eafm5bgtX67XDZQ8CWKSGu9DekI+yN4Y5I=
k8s.io/gengo v0.0.0-20190116091435-f8a0810f38af h1:SwjZbO0u5ZuaV6TRMWOGB40iaycX8sbdMQHtjNZ19dk=
k8s.io/gengo v0.0.0-20190116091435-f8a0810f38af/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= k8s.io/gengo v0.0.0-20190116091435-f8a0810f38af/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=
k8s.io/klog v0.3.1 h1:RVgyDHY/kFKtLqh67NvEWIgkMneNoIrdkN0CxDSQc68=
k8s.io/klog v0.3.1/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= k8s.io/klog v0.3.1/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
k8s.io/kube-openapi v0.0.0-20190228160746-b3a7cee44a30/go.mod h1:BXM9ceUBTj2QnfH2MK1odQs778ajze1RxcmP6S8RVVc= k8s.io/kube-openapi v0.0.0-20190228160746-b3a7cee44a30/go.mod h1:BXM9ceUBTj2QnfH2MK1odQs778ajze1RxcmP6S8RVVc=
k8s.io/utils v0.0.0-20190221042446-c2654d5206da/go.mod h1:8k8uAuAQ0rXslZKaEWd0c3oVhZz7sSzSiPnVZayjIX0= k8s.io/utils v0.0.0-20190221042446-c2654d5206da/go.mod h1:8k8uAuAQ0rXslZKaEWd0c3oVhZz7sSzSiPnVZayjIX0=

View File

@ -59,19 +59,29 @@ func ValidateCookie(r *http.Request, c *http.Cookie) (string, error) {
// ValidateEmail checks if the given email address matches either a whitelisted // ValidateEmail checks if the given email address matches either a whitelisted
// email address, as defined by the "whitelist" config parameter. Or is part of // email address, as defined by the "whitelist" config parameter. Or is part of
// a permitted domain, as defined by the "domains" config parameter // a permitted domain, as defined by the "domains" config parameter
func ValidateEmail(email string) bool { func ValidateEmail(email, ruleName string) bool {
// 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? // Do we have any validation to perform?
if len(config.Whitelist) == 0 && len(config.Domains) == 0 { if len(whitelist) == 0 && len(domains) == 0 {
return true return true
} }
// Email whitelist validation // Email whitelist validation
if len(config.Whitelist) > 0 { if len(whitelist) > 0 {
for _, whitelist := range config.Whitelist { if ValidateWhitelist(email, whitelist) {
if email == whitelist {
return true return true
} }
}
// If we're not matching *either*, stop here // If we're not matching *either*, stop here
if !config.MatchWhitelistOrDomain { if !config.MatchWhitelistOrDomain {
@ -80,18 +90,34 @@ func ValidateEmail(email string) bool {
} }
// Domain validation // Domain validation
if len(config.Domains) > 0 { if len(domains) > 0 && ValidateDomains(email, domains) {
return true
}
return false
}
// ValidateWhitelist checks if the email is in whitelist
func ValidateWhitelist(email string, whitelist CommaSeparatedList) bool {
for _, whitelist := range whitelist {
if email == whitelist {
return true
}
}
return false
}
// ValidateDomains checks if the email matches a whitelisted domain
func ValidateDomains(email string, domains CommaSeparatedList) bool {
parts := strings.Split(email, "@") 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] {
return true return true
} }
} }
}
return false return false
} }
@ -99,24 +125,19 @@ func ValidateEmail(email string) bool {
// 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)
@ -129,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)
@ -170,23 +191,31 @@ func ClearCookie(r *http.Request) *http.Cookie {
} }
} }
func buildCSRFCookieName(nonce string) string {
return config.CSRFCookieName + "_" + nonce[:6]
}
// MakeCSRFCookie makes a csrf cookie (used during login only) // 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),
} }
} }
// ClearCSRFCookie makes an expired csrf 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),
@ -196,16 +225,16 @@ func ClearCSRFCookie(r *http.Request) *http.Cookie {
} }
} }
// ValidateCSRFCookie validates the csrf cookie against state // FindCSRFCookie extracts the CSRF cookie from the request based on state.
func ValidateCSRFCookie(r *http.Request, c *http.Cookie) (valid bool, provider string, redirect string, err error) { func FindCSRFCookie(r *http.Request, state string) (c *http.Cookie, err error) {
state := r.URL.Query().Get("state") // Check for CSRF cookie
return r.Cookie(buildCSRFCookieName(state))
if len(c.Value) != 32 {
return false, "", "", errors.New("Invalid CSRF cookie value")
} }
if len(state) < 34 { // ValidateCSRFCookie validates the csrf cookie against state
return false, "", "", errors.New("Invalid CSRF state value") func ValidateCSRFCookie(c *http.Cookie, state string) (valid bool, provider string, redirect string, err error) {
if len(c.Value) != 32 {
return false, "", "", errors.New("Invalid CSRF cookie value")
} }
// Check nonce match // Check nonce match
@ -229,6 +258,14 @@ func MakeState(r *http.Request, p provider.Provider, nonce string) string {
return fmt.Sprintf("%s:%s:%s", nonce, p.Name(), returnUrl(r)) return fmt.Sprintf("%s:%s:%s", nonce, p.Name(), returnUrl(r))
} }
// ValidateState checks whether the state is of right length.
func ValidateState(state string) error {
if len(state) < 34 {
return errors.New("Invalid CSRF state value")
}
return nil
}
// Nonce generates a random nonce // Nonce generates a random nonce
func Nonce() (error, string) { func Nonce() (error, string) {
nonce := make([]byte, 16) nonce := make([]byte, 16)
@ -242,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
} }
@ -255,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

View File

@ -1,8 +1,8 @@
package tfa package tfa
import ( import (
"fmt"
"net/http" "net/http"
"net/http/httptest"
"net/url" "net/url"
"strings" "strings"
"testing" "testing"
@ -66,32 +66,25 @@ 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") assert.True(v, "should allow user in whitelist")
// Should allow only matching email address when // Should allow only matching email address when
@ -99,33 +92,113 @@ func TestAuthValidateEmail(t *testing.T) {
config.Domains = []string{"example.com"} config.Domains = []string{"example.com"}
config.Whitelist = []string{"test@test.com"} config.Whitelist = []string{"test@test.com"}
config.MatchWhitelistOrDomain = false config.MatchWhitelistOrDomain = false
v = ValidateEmail("test@test.com") v = ValidateEmail("one@two.com", "default")
assert.True(v, "should allow user in whitelist")
v = ValidateEmail("test@example.com")
assert.False(v, "should not allow user from valid domain")
v = ValidateEmail("one@two.com")
assert.False(v, "should not allow user not in either") 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 // Should allow either matching domain or email address when
// MatchWhitelistOrDomain is enabled // MatchWhitelistOrDomain is enabled
config.Domains = []string{"example.com"} config.Domains = []string{"example.com"}
config.Whitelist = []string{"test@test.com"} config.Whitelist = []string{"test@test.com"}
config.MatchWhitelistOrDomain = true config.MatchWhitelistOrDomain = true
v = ValidateEmail("test@test.com") v = ValidateEmail("one@two.com", "default")
assert.True(v, "should allow user in whitelist")
v = ValidateEmail("test@example.com")
assert.True(v, "should allow user from valid domain")
v = ValidateEmail("one@two.com")
assert.False(v, "should not allow user not in either") 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")
} }
func TestRedirectUri(t *testing.T) { func TestRedirectUri(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
r, _ := http.NewRequest("GET", "http://example.com", nil) r := httptest.NewRequest("GET", "http://app.example.com/hello", nil)
r.Header.Add("X-Forwarded-Proto", "http") r.Header.Add("X-Forwarded-Proto", "http")
r.Header.Add("X-Forwarded-Host", "app.example.com")
r.Header.Add("X-Forwarded-Uri", "/hello")
// //
// No Auth Host // No Auth Host
@ -167,10 +240,8 @@ func TestRedirectUri(t *testing.T) {
// With Auth URL + cookie domain, but from different domain // With Auth URL + cookie domain, but from different domain
// - will not use auth host // - will not use auth host
// //
r, _ = http.NewRequest("GET", "http://another.com", nil) r = httptest.NewRequest("GET", "https://another.com/hello", nil)
r.Header.Add("X-Forwarded-Proto", "https") r.Header.Add("X-Forwarded-Proto", "https")
r.Header.Add("X-Forwarded-Host", "another.com")
r.Header.Add("X-Forwarded-Uri", "/hello")
config.AuthHost = "auth.example.com" config.AuthHost = "auth.example.com"
config.CookieDomains = []CookieDomain{*NewCookieDomain("example.com")} config.CookieDomains = []CookieDomain{*NewCookieDomain("example.com")}
@ -217,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")
} }
@ -249,63 +321,62 @@ func TestAuthValidateCSRFCookie(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
config, _ = NewConfig([]string{}) config, _ = NewConfig([]string{})
c := &http.Cookie{} c := &http.Cookie{}
state := ""
newCsrfRequest := func(state string) *http.Request {
u := fmt.Sprintf("http://example.com?state=%s", state)
r, _ := http.NewRequest("GET", u, nil)
return r
}
// Should require 32 char string // Should require 32 char string
r := newCsrfRequest("") state = ""
c.Value = "" c.Value = ""
valid, _, _, err := ValidateCSRFCookie(r, c) valid, _, _, err := ValidateCSRFCookie(c, state)
assert.False(valid) assert.False(valid)
if assert.Error(err) { if assert.Error(err) {
assert.Equal("Invalid CSRF cookie value", err.Error()) assert.Equal("Invalid CSRF cookie value", err.Error())
} }
c.Value = "123456789012345678901234567890123" c.Value = "123456789012345678901234567890123"
valid, _, _, err = ValidateCSRFCookie(r, c) valid, _, _, err = ValidateCSRFCookie(c, state)
assert.False(valid) assert.False(valid)
if assert.Error(err) { if assert.Error(err) {
assert.Equal("Invalid CSRF cookie value", err.Error()) assert.Equal("Invalid CSRF cookie value", err.Error())
} }
// Should require valid state
r = newCsrfRequest("12345678901234567890123456789012:")
c.Value = "12345678901234567890123456789012"
valid, _, _, err = ValidateCSRFCookie(r, c)
assert.False(valid)
if assert.Error(err) {
assert.Equal("Invalid CSRF state value", err.Error())
}
// Should require provider // Should require provider
r = newCsrfRequest("12345678901234567890123456789012:99") state = "12345678901234567890123456789012:99"
c.Value = "12345678901234567890123456789012" c.Value = "12345678901234567890123456789012"
valid, _, _, err = ValidateCSRFCookie(r, c) valid, _, _, err = ValidateCSRFCookie(c, state)
assert.False(valid) assert.False(valid)
if assert.Error(err) { if assert.Error(err) {
assert.Equal("Invalid CSRF state format", err.Error()) assert.Equal("Invalid CSRF state format", err.Error())
} }
// Should allow valid state // Should allow valid state
r = newCsrfRequest("12345678901234567890123456789012:p99:url123") state = "12345678901234567890123456789012:p99:url123"
c.Value = "12345678901234567890123456789012" c.Value = "12345678901234567890123456789012"
valid, provider, redirect, err := ValidateCSRFCookie(r, c) valid, provider, redirect, err := ValidateCSRFCookie(c, state)
assert.True(valid, "valid request should return valid") assert.True(valid, "valid request should return valid")
assert.Nil(err, "valid request should not return an error") assert.Nil(err, "valid request should not return an error")
assert.Equal("p99", provider, "valid request should return correct provider") assert.Equal("p99", provider, "valid request should return correct provider")
assert.Equal("url123", redirect, "valid request should return correct redirect") assert.Equal("url123", redirect, "valid request should return correct redirect")
} }
func TestValidateState(t *testing.T) {
assert := assert.New(t)
// Should require valid state
state := "12345678901234567890123456789012:"
err := ValidateState(state)
if assert.Error(err) {
assert.Equal("Invalid CSRF state value", err.Error())
}
// Should pass this state
state = "12345678901234567890123456789012:p99:url123"
err = ValidateState(state)
assert.Nil(err, "valid request should not return an error")
}
func TestMakeState(t *testing.T) { func TestMakeState(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
r, _ := http.NewRequest("GET", "http://example.com", nil) r := httptest.NewRequest("GET", "http://example.com/hello", nil)
r.Header.Add("X-Forwarded-Proto", "http") r.Header.Add("X-Forwarded-Proto", "http")
r.Header.Add("X-Forwarded-Host", "example.com")
r.Header.Add("X-Forwarded-Uri", "/hello")
// Test with google // Test with google
p := provider.Google{} p := provider.Google{}

View File

@ -39,6 +39,7 @@ type Config struct {
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)" json:"-"` SecretString string `long:"secret" env:"SECRET" description:"Secret used for signing (required)" json:"-"`
Whitelist CommaSeparatedList `long:"whitelist" env:"WHITELIST" env-delim:"," description:"Only allow given email addresses, can be set multiple times"` Whitelist CommaSeparatedList `long:"whitelist" env:"WHITELIST" env-delim:"," description:"Only allow given email addresses, can be set multiple times"`
Port int `long:"port" env:"PORT" default:"4181" description:"Port to listen on"`
Providers provider.Providers `group:"providers" namespace:"providers" env-namespace:"PROVIDERS"` Providers provider.Providers `group:"providers" namespace:"providers" env-namespace:"PROVIDERS"`
Rules map[string]*Rule `long:"rule.<name>.<param>" description:"Rule definitions, param can be: \"action\", \"rule\" or \"provider\""` Rules map[string]*Rule `long:"rule.<name>.<param>" description:"Rule definitions, param can be: \"action\", \"rule\" or \"provider\""`
@ -47,6 +48,9 @@ type Config struct {
Secret []byte `json:"-"` Secret []byte `json:"-"`
Lifetime time.Duration Lifetime time.Duration
// Authorization
RequiredRole string `long:"required-role" env:"REQUIRED_ROLE" description:"Required role to verify authorization"`
// Legacy // Legacy
CookieDomainsLegacy CookieDomains `long:"cookie-domains" env:"COOKIE_DOMAINS" description:"DEPRECATED - Use \"cookie-domain\""` CookieDomainsLegacy CookieDomains `long:"cookie-domains" env:"COOKIE_DOMAINS" description:"DEPRECATED - Use \"cookie-domain\""`
CookieSecretLegacy string `long:"cookie-secret" env:"COOKIE_SECRET" description:"DEPRECATED - Use \"secret\"" json:"-"` CookieSecretLegacy string `long:"cookie-secret" env:"COOKIE_SECRET" description:"DEPRECATED - Use \"secret\"" json:"-"`
@ -210,6 +214,14 @@ func (c *Config) parseUnknownFlag(option string, arg flags.SplitArgument, args [
rule.Rule = val rule.Rule = val
case "provider": case "provider":
rule.Provider = val rule.Provider = val
case "whitelist":
list := CommaSeparatedList{}
list.UnmarshalFlag(val)
rule.Whitelist = list
case "domains":
list := CommaSeparatedList{}
list.UnmarshalFlag(val)
rule.Domains = list
default: default:
return args, fmt.Errorf("invalid route param: %v", option) return args, fmt.Errorf("invalid route param: %v", option)
} }
@ -317,7 +329,7 @@ func (c *Config) setupProvider(name string) error {
} }
// Setup // Setup
err = p.Setup() err = p.Setup(log)
if err != nil { if err != nil {
return err return err
} }
@ -330,6 +342,8 @@ 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 // NewRule creates a new rule object

View File

@ -37,6 +37,7 @@ func TestConfigDefaults(t *testing.T) {
assert.False(c.MatchWhitelistOrDomain) assert.False(c.MatchWhitelistOrDomain)
assert.Equal("/_oauth", c.Path) assert.Equal("/_oauth", c.Path)
assert.Len(c.Whitelist, 0) assert.Len(c.Whitelist, 0)
assert.Equal(c.Port, 4181)
assert.Equal("select_account", c.Providers.Google.Prompt) assert.Equal("select_account", c.Providers.Google.Prompt)
} }
@ -51,6 +52,7 @@ func TestConfigParseArgs(t *testing.T) {
"--rule.1.rule=PathPrefix(`/one`)", "--rule.1.rule=PathPrefix(`/one`)",
"--rule.two.action=auth", "--rule.two.action=auth",
"--rule.two.rule=\"Host(`two.com`) && Path(`/two`)\"", "--rule.two.rule=\"Host(`two.com`) && Path(`/two`)\"",
"--port=8000",
}) })
require.Nil(t, err) require.Nil(t, err)
@ -58,6 +60,7 @@ func TestConfigParseArgs(t *testing.T) {
assert.Equal("cookiename", c.CookieName) assert.Equal("cookiename", c.CookieName)
assert.Equal("csrfcookiename", c.CSRFCookieName) assert.Equal("csrfcookiename", c.CSRFCookieName)
assert.Equal("oidc", c.DefaultProvider) assert.Equal("oidc", c.DefaultProvider)
assert.Equal(8000, c.Port)
// Check rules // Check rules
assert.Equal(map[string]*Rule{ assert.Equal(map[string]*Rule{

View File

@ -8,6 +8,8 @@ import (
"net/http" "net/http"
"golang.org/x/oauth2" "golang.org/x/oauth2"
"github.com/sirupsen/logrus"
) )
// GenericOAuth provider // GenericOAuth provider
@ -29,7 +31,7 @@ func (o *GenericOAuth) Name() string {
} }
// Setup performs validation and setup // Setup performs validation and setup
func (o *GenericOAuth) Setup() error { func (o *GenericOAuth) Setup(log *logrus.Logger) error {
// Check parmas // Check parmas
if o.AuthURL == "" || o.TokenURL == "" || o.UserURL == "" || o.ClientID == "" || o.ClientSecret == "" { 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") 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")
@ -67,12 +69,13 @@ func (o *GenericOAuth) ExchangeCode(redirectURI, code string) (string, error) {
} }
// GetUser uses the given token and returns a complete provider.User object // GetUser uses the given token and returns a complete provider.User object
func (o *GenericOAuth) GetUser(token string) (User, error) { func (o *GenericOAuth) GetUser(token string) (User, Roles, error) {
var user User var user User
var roles Roles
req, err := http.NewRequest("GET", o.UserURL, nil) req, err := http.NewRequest("GET", o.UserURL, nil)
if err != nil { if err != nil {
return user, err return user, roles, err
} }
if o.TokenStyle == "header" { if o.TokenStyle == "header" {
@ -86,11 +89,11 @@ func (o *GenericOAuth) GetUser(token string) (User, error) {
client := &http.Client{} client := &http.Client{}
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
} }

View File

@ -6,6 +6,8 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"net/url" "net/url"
"github.com/sirupsen/logrus"
) )
// Google provider // Google provider
@ -26,7 +28,7 @@ func (g *Google) Name() string {
} }
// Setup performs validation and setup // Setup performs validation and setup
func (g *Google) Setup() error { func (g *Google) Setup(log *logrus.Logger) error {
if g.ClientID == "" || g.ClientSecret == "" { if g.ClientID == "" || g.ClientSecret == "" {
return errors.New("providers.google.client-id, providers.google.client-secret must be set") return errors.New("providers.google.client-id, providers.google.client-secret must be set")
} }
@ -93,23 +95,24 @@ func (g *Google) ExchangeCode(redirectURI, code string) (string, error) {
} }
// GetUser uses the given token and returns a complete provider.User object // GetUser uses the given token and returns a complete provider.User object
func (g *Google) GetUser(token string) (User, error) { func (g *Google) GetUser(token string) (User, Roles, error) {
var user User var user User
var roles Roles
client := &http.Client{} client := &http.Client{}
req, err := http.NewRequest("GET", g.UserURL.String(), nil) req, err := http.NewRequest("GET", g.UserURL.String(), nil)
if err != nil { if err != nil {
return user, err return user, roles, err
} }
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token)) req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
res, err := client.Do(req) res, err := client.Do(req)
if err != nil { if err != nil {
return user, err return user, roles, err
} }
defer res.Body.Close() defer res.Body.Close()
err = json.NewDecoder(res.Body).Decode(&user) err = json.NewDecoder(res.Body).Decode(&user)
return user, err return user, roles, err
} }

View File

@ -6,6 +6,8 @@ import (
"github.com/coreos/go-oidc" "github.com/coreos/go-oidc"
"golang.org/x/oauth2" "golang.org/x/oauth2"
"github.com/sirupsen/logrus"
) )
// OIDC provider // OIDC provider
@ -18,6 +20,8 @@ type OIDC struct {
provider *oidc.Provider provider *oidc.Provider
verifier *oidc.IDTokenVerifier verifier *oidc.IDTokenVerifier
log *logrus.Logger
} }
// Name returns the name of the provider // Name returns the name of the provider
@ -26,7 +30,9 @@ func (o *OIDC) Name() string {
} }
// Setup performs validation and setup // Setup performs validation and setup
func (o *OIDC) Setup() error { func (o *OIDC) Setup(log *logrus.Logger) error {
o.log = log
// Check parms // Check parms
if o.IssuerURL == "" || o.ClientID == "" || o.ClientSecret == "" { if o.IssuerURL == "" || o.ClientID == "" || o.ClientSecret == "" {
return errors.New("providers.oidc.issuer-url, providers.oidc.client-id, providers.oidc.client-secret must be set") return errors.New("providers.oidc.issuer-url, providers.oidc.client-id, providers.oidc.client-secret must be set")
@ -70,6 +76,7 @@ func (o *OIDC) ExchangeCode(redirectURI, code string) (string, error) {
if err != nil { if err != nil {
return "", err return "", err
} }
o.log.WithField("accessToken", token.AccessToken).Debug("getUser")
// Extract ID token // Extract ID token
rawIDToken, ok := token.Extra("id_token").(string) rawIDToken, ok := token.Extra("id_token").(string)
@ -81,19 +88,26 @@ func (o *OIDC) ExchangeCode(redirectURI, code string) (string, error) {
} }
// GetUser uses the given token and returns a complete provider.User object // GetUser uses the given token and returns a complete provider.User object
func (o *OIDC) GetUser(token string) (User, error) { func (o *OIDC) GetUser(token string) (User, Roles, error) {
var user User var user User
var roles Roles
// Parse & Verify ID Token // Parse & Verify ID Token
idToken, err := o.verifier.Verify(o.ctx, token) idToken, err := o.verifier.Verify(o.ctx, token)
if err != nil { if err != nil {
return user, err return user, roles, err
} }
// Extract custom claims // Extract custom claims
if err := idToken.Claims(&user); err != nil { if err := idToken.Claims(&user); err != nil {
return user, err return user, roles, err
} }
o.log.WithField("user", user).Debug("getUser")
return user, nil if err := idToken.Claims(&roles); err != nil {
return user, roles, err
}
o.log.WithField("roles", roles).Debug("getUser")
return user, roles, nil
} }

View File

@ -5,6 +5,7 @@ import (
// "net/url" // "net/url"
"golang.org/x/oauth2" "golang.org/x/oauth2"
"github.com/sirupsen/logrus"
) )
// Providers contains all the implemented providers // Providers contains all the implemented providers
@ -19,8 +20,8 @@ type Provider interface {
Name() string Name() string
GetLoginURL(redirectURI, state string) string GetLoginURL(redirectURI, state string) string
ExchangeCode(redirectURI, code string) (string, error) ExchangeCode(redirectURI, code string) (string, error)
GetUser(token string) (User, error) GetUser(token string) (User, Roles, error)
Setup() error Setup(*logrus.Logger) error
} }
type token struct { type token struct {
@ -32,6 +33,10 @@ type User struct {
Email string `json:"email"` Email string `json:"email"`
} }
type Roles struct {
Roles []string `json:"roles"`
}
// OAuthProvider is a provider using the oauth2 library // OAuthProvider is a provider using the oauth2 library
type OAuthProvider struct { type OAuthProvider struct {
Resource string `long:"resource" env:"RESOURCE" description:"Optional resource indicator"` Resource string `long:"resource" env:"RESOURCE" description:"Optional resource indicator"`

View File

@ -58,7 +58,11 @@ func (s *Server) RootHandler(w http.ResponseWriter, r *http.Request) {
// Modify request // Modify request
r.Method = r.Header.Get("X-Forwarded-Method") r.Method = r.Header.Get("X-Forwarded-Method")
r.Host = r.Header.Get("X-Forwarded-Host") r.Host = r.Header.Get("X-Forwarded-Host")
// Read URI from header if we're acting as forward auth middleware
if _, ok := r.Header["X-Forwarded-Uri"]; ok {
r.URL, _ = url.Parse(r.Header.Get("X-Forwarded-Uri")) r.URL, _ = url.Parse(r.Header.Get("X-Forwarded-Uri"))
}
// Pass to mux // Pass to mux
s.router.ServeHTTP(w, r) s.router.ServeHTTP(w, r)
@ -101,7 +105,7 @@ func (s *Server) AuthHandler(providerName, rule string) http.HandlerFunc {
} }
// Validate user // Validate user
valid := ValidateEmail(email) valid := ValidateEmail(email, rule)
if !valid { if !valid {
logger.WithField("email", email).Warn("Invalid email") logger.WithField("email", email).Warn("Invalid email")
http.Error(w, "Not authorized", 401) http.Error(w, "Not authorized", 401)
@ -121,16 +125,26 @@ func (s *Server) AuthCallbackHandler() http.HandlerFunc {
// Logging setup // Logging setup
logger := s.logger(r, "AuthCallback", "default", "Handling callback") logger := s.logger(r, "AuthCallback", "default", "Handling callback")
// Check state
state := r.URL.Query().Get("state")
if err := ValidateState(state); err != nil {
logger.WithFields(logrus.Fields{
"error": err,
}).Warn("Error validating state")
http.Error(w, "Not authorized", 401)
return
}
// Check for CSRF cookie // Check for CSRF cookie
c, err := r.Cookie(config.CSRFCookieName) c, err := FindCSRFCookie(r, state)
if err != nil { if err != nil {
logger.Info("Missing csrf cookie") logger.Info("Missing csrf cookie")
http.Error(w, "Not authorized", 401) http.Error(w, "Not authorized", 401)
return return
} }
// Validate state // Validate CSRF cookie against state
valid, providerName, redirect, err := ValidateCSRFCookie(r, c) valid, providerName, redirect, err := ValidateCSRFCookie(c, state)
if !valid { if !valid {
logger.WithFields(logrus.Fields{ logger.WithFields(logrus.Fields{
"error": err, "error": err,
@ -153,7 +167,7 @@ func (s *Server) AuthCallbackHandler() http.HandlerFunc {
} }
// Clear CSRF cookie // Clear CSRF cookie
http.SetCookie(w, ClearCSRFCookie(r)) http.SetCookie(w, ClearCSRFCookie(r, c))
// Exchange code for token // Exchange code for token
token, err := p.ExchangeCode(redirectUri(r), r.URL.Query().Get("code")) token, err := p.ExchangeCode(redirectUri(r), r.URL.Query().Get("code"))
@ -164,12 +178,23 @@ func (s *Server) AuthCallbackHandler() http.HandlerFunc {
} }
// Get user // Get user
user, err := p.GetUser(token) user, roles, err := p.GetUser(token)
if err != nil { if err != nil {
logger.WithField("error", err).Error("Error getting user") logger.WithField("error", err).Error("Error getting user")
http.Error(w, "Service unavailable", 503) http.Error(w, "Service unavailable", 503)
return return
} }
found := false
for _, r := range roles.Roles {
if r == config.RequiredRole {
found = true
}
}
if ! found {
logger.Debug("required role not found, deny access")
http.Error(w, "Forbidden", 403)
return
}
// Generate cookie // Generate cookie
http.SetCookie(w, MakeCookie(r, user.Email)) http.SetCookie(w, MakeCookie(r, user.Email))
@ -177,6 +202,7 @@ func (s *Server) AuthCallbackHandler() http.HandlerFunc {
"provider": providerName, "provider": providerName,
"redirect": redirect, "redirect": redirect,
"user": user.Email, "user": user.Email,
"roles": roles.Roles,
}).Info("Successfully generated auth cookie, redirecting user.") }).Info("Successfully generated auth cookie, redirecting user.")
// Redirect // Redirect

View File

@ -31,6 +31,37 @@ func init() {
* Tests * Tests
*/ */
func TestServerRootHandler(t *testing.T) {
assert := assert.New(t)
config = newDefaultConfig()
// X-Forwarded headers should be read into request
req := httptest.NewRequest("POST", "http://should-use-x-forwarded.com/should?ignore=me", nil)
req.Header.Add("X-Forwarded-Method", "GET")
req.Header.Add("X-Forwarded-Proto", "https")
req.Header.Add("X-Forwarded-Host", "example.com")
req.Header.Add("X-Forwarded-Uri", "/foo?q=bar")
NewServer().RootHandler(httptest.NewRecorder(), req)
assert.Equal("GET", req.Method, "x-forwarded-method should be read into request")
assert.Equal("example.com", req.Host, "x-forwarded-host should be read into request")
assert.Equal("/foo", req.URL.Path, "x-forwarded-uri should be read into request")
assert.Equal("/foo?q=bar", req.URL.RequestURI(), "x-forwarded-uri should be read into request")
// Other X-Forwarded headers should be read in into request and original URL
// should be preserved if X-Forwarded-Uri not present
req = httptest.NewRequest("POST", "http://should-use-x-forwarded.com/should-not?ignore=me", nil)
req.Header.Add("X-Forwarded-Method", "GET")
req.Header.Add("X-Forwarded-Proto", "https")
req.Header.Add("X-Forwarded-Host", "example.com")
NewServer().RootHandler(httptest.NewRecorder(), req)
assert.Equal("GET", req.Method, "x-forwarded-method should be read into request")
assert.Equal("example.com", req.Host, "x-forwarded-host should be read into request")
assert.Equal("/should-not", req.URL.Path, "request url should be preserved if x-forwarded-uri not present")
assert.Equal("/should-not?ignore=me", req.URL.RequestURI(), "request url should be preserved if x-forwarded-uri not present")
}
func TestServerAuthHandlerInvalid(t *testing.T) { func TestServerAuthHandlerInvalid(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
config = newDefaultConfig() config = newDefaultConfig()
@ -90,15 +121,15 @@ func TestServerAuthHandlerExpired(t *testing.T) {
config.Domains = []string{"test.com"} config.Domains = []string{"test.com"}
// Should redirect expired cookie // Should redirect expired cookie
req := newDefaultHttpRequest("/foo") req := newHTTPRequest("GET", "http://example.com/foo")
c := MakeCookie(req, "test@example.com") c := MakeCookie(req, "test@example.com")
res, _ := doHttpRequest(req, c) res, _ := doHttpRequest(req, c)
assert.Equal(307, res.StatusCode, "request with expired cookie should be redirected") require.Equal(t, 307, res.StatusCode, "request with expired cookie should be redirected")
// Check for CSRF cookie // Check for CSRF cookie
var cookie *http.Cookie var cookie *http.Cookie
for _, c := range res.Cookies() { for _, c := range res.Cookies() {
if c.Name == config.CSRFCookieName { if strings.HasPrefix(c.Name, config.CSRFCookieName) {
cookie = c cookie = c
} }
} }
@ -116,7 +147,7 @@ func TestServerAuthHandlerValid(t *testing.T) {
config = newDefaultConfig() config = newDefaultConfig()
// Should allow valid request email // Should allow valid request email
req := newDefaultHttpRequest("/foo") req := newHTTPRequest("GET", "http://example.com/foo")
c := MakeCookie(req, "test@example.com") c := MakeCookie(req, "test@example.com")
config.Domains = []string{} config.Domains = []string{}
@ -131,6 +162,7 @@ func TestServerAuthHandlerValid(t *testing.T) {
func TestServerAuthCallback(t *testing.T) { func TestServerAuthCallback(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
require := require.New(t)
config = newDefaultConfig() config = newDefaultConfig()
// Setup OAuth server // Setup OAuth server
@ -148,27 +180,28 @@ func TestServerAuthCallback(t *testing.T) {
} }
// Should pass auth response request to callback // Should pass auth response request to callback
req := newDefaultHttpRequest("/_oauth") req := newHTTPRequest("GET", "http://example.com/_oauth")
res, _ := doHttpRequest(req, nil) res, _ := doHttpRequest(req, nil)
assert.Equal(401, res.StatusCode, "auth callback without cookie shouldn't be authorised") assert.Equal(401, res.StatusCode, "auth callback without cookie shouldn't be authorised")
// Should catch invalid csrf cookie // Should catch invalid csrf cookie
req = newDefaultHttpRequest("/_oauth?state=12345678901234567890123456789012:http://redirect") nonce := "12345678901234567890123456789012"
req = newHTTPRequest("GET", "http://example.com/_oauth?state="+nonce+":http://redirect")
c := MakeCSRFCookie(req, "nononononononononononononononono") c := MakeCSRFCookie(req, "nononononononononononononononono")
res, _ = doHttpRequest(req, c) res, _ = doHttpRequest(req, c)
assert.Equal(401, res.StatusCode, "auth callback with invalid cookie shouldn't be authorised") assert.Equal(401, res.StatusCode, "auth callback with invalid cookie shouldn't be authorised")
// Should catch invalid provider cookie // Should catch invalid provider cookie
req = newDefaultHttpRequest("/_oauth?state=12345678901234567890123456789012:invalid:http://redirect") req = newHTTPRequest("GET", "http://example.com/_oauth?state="+nonce+":invalid:http://redirect")
c = MakeCSRFCookie(req, "12345678901234567890123456789012") c = MakeCSRFCookie(req, nonce)
res, _ = doHttpRequest(req, c) res, _ = doHttpRequest(req, c)
assert.Equal(401, res.StatusCode, "auth callback with invalid provider shouldn't be authorised") assert.Equal(401, res.StatusCode, "auth callback with invalid provider shouldn't be authorised")
// Should redirect valid request // Should redirect valid request
req = newDefaultHttpRequest("/_oauth?state=12345678901234567890123456789012:google:http://redirect") req = newHTTPRequest("GET", "http://example.com/_oauth?state="+nonce+":google:http://redirect")
c = MakeCSRFCookie(req, "12345678901234567890123456789012") c = MakeCSRFCookie(req, nonce)
res, _ = doHttpRequest(req, c) res, _ = doHttpRequest(req, c)
assert.Equal(307, res.StatusCode, "valid auth callback should be allowed") require.Equal(307, res.StatusCode, "valid auth callback should be allowed")
fwd, _ := res.Location() fwd, _ := res.Location()
assert.Equal("http", fwd.Scheme, "valid request should be redirected to return url") assert.Equal("http", fwd.Scheme, "valid request should be redirected to return url")
@ -360,17 +393,17 @@ func TestServerRouteHost(t *testing.T) {
} }
// Should block any request // Should block any request
req := newHttpRequest("GET", "https://example.com/", "/") req := newHTTPRequest("GET", "https://example.com/")
res, _ := doHttpRequest(req, nil) res, _ := doHttpRequest(req, nil)
assert.Equal(307, res.StatusCode, "request not matching any rule should require auth") assert.Equal(307, res.StatusCode, "request not matching any rule should require auth")
// Should allow matching request // Should allow matching request
req = newHttpRequest("GET", "https://api.example.com/", "/") req = newHTTPRequest("GET", "https://api.example.com/")
res, _ = doHttpRequest(req, nil) res, _ = doHttpRequest(req, nil)
assert.Equal(200, res.StatusCode, "request matching allow rule should be allowed") assert.Equal(200, res.StatusCode, "request matching allow rule should be allowed")
// Should allow matching request // Should allow matching request
req = newHttpRequest("GET", "https://sub8.example.com/", "/") req = newHTTPRequest("GET", "https://sub8.example.com/")
res, _ = doHttpRequest(req, nil) res, _ = doHttpRequest(req, nil)
assert.Equal(200, res.StatusCode, "request matching allow rule should be allowed") assert.Equal(200, res.StatusCode, "request matching allow rule should be allowed")
} }
@ -386,12 +419,12 @@ func TestServerRouteMethod(t *testing.T) {
} }
// Should block any request // Should block any request
req := newHttpRequest("GET", "https://example.com/", "/") req := newHTTPRequest("GET", "https://example.com/")
res, _ := doHttpRequest(req, nil) res, _ := doHttpRequest(req, nil)
assert.Equal(307, res.StatusCode, "request not matching any rule should require auth") assert.Equal(307, res.StatusCode, "request not matching any rule should require auth")
// Should allow matching request // Should allow matching request
req = newHttpRequest("PUT", "https://example.com/", "/") req = newHTTPRequest("PUT", "https://example.com/")
res, _ = doHttpRequest(req, nil) res, _ = doHttpRequest(req, nil)
assert.Equal(200, res.StatusCode, "request matching allow rule should be allowed") assert.Equal(200, res.StatusCode, "request matching allow rule should be allowed")
} }
@ -441,12 +474,12 @@ func TestServerRouteQuery(t *testing.T) {
} }
// Should block any request // Should block any request
req := newHttpRequest("GET", "https://example.com/", "/?q=no") req := newHTTPRequest("GET", "https://example.com/?q=no")
res, _ := doHttpRequest(req, nil) res, _ := doHttpRequest(req, nil)
assert.Equal(307, res.StatusCode, "request not matching any rule should require auth") assert.Equal(307, res.StatusCode, "request not matching any rule should require auth")
// Should allow matching request // Should allow matching request
req = newHttpRequest("GET", "https://api.example.com/", "/?q=test123") req = newHTTPRequest("GET", "https://api.example.com/?q=test123")
res, _ = doHttpRequest(req, nil) res, _ = doHttpRequest(req, nil)
assert.Equal(200, res.StatusCode, "request matching allow rule should be allowed") assert.Equal(200, res.StatusCode, "request matching allow rule should be allowed")
} }
@ -531,16 +564,17 @@ func newDefaultConfig() *Config {
return config return config
} }
// TODO: replace with newHTTPRequest("GET", "http://example.com/"+uri)
func newDefaultHttpRequest(uri string) *http.Request { func newDefaultHttpRequest(uri string) *http.Request {
return newHttpRequest("", "http://example.com/", uri) return newHTTPRequest("GET", "http://example.com"+uri)
} }
func newHttpRequest(method, dest, uri string) *http.Request { func newHTTPRequest(method, target string) *http.Request {
r := httptest.NewRequest("", "http://should-use-x-forwarded.com", nil) u, _ := url.Parse(target)
p, _ := url.Parse(dest) r := httptest.NewRequest(method, target, nil)
r.Header.Add("X-Forwarded-Method", method) r.Header.Add("X-Forwarded-Method", method)
r.Header.Add("X-Forwarded-Proto", p.Scheme) r.Header.Add("X-Forwarded-Proto", u.Scheme)
r.Header.Add("X-Forwarded-Host", p.Host) r.Header.Add("X-Forwarded-Host", u.Host)
r.Header.Add("X-Forwarded-Uri", uri) r.Header.Add("X-Forwarded-Uri", u.RequestURI())
return r return r
} }