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
.travis.yml
.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
Copyright (c) [2023] [Wolfgang Hottgenroth]
Copyright (c) [2018] [Thom Seddon]
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.
@ -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))
- 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 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))
@ -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`).
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.
@ -92,7 +94,7 @@ services:
#### 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.
@ -162,6 +164,7 @@ Application Options:
--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:
@ -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:
- `auth` (default)
- `allow`
- `domains` - optional, same usage as [`domain`](#domain)
- `provider` - same usage as [`default-provider`](#default-provider), supported values:
- `google`
- `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]+}`, ...)``
- ``PathPrefix(`/products/`, `/articles/{category}/{id:[0-9]+}`)``
- ``Query(`foo=bar`, `bar=baz`)``
- `whitelist` - optional, same usage as whitelist`](#whitelist)
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.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.
@ -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
* `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
@ -416,8 +426,6 @@ spec:
- 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.
#### Selective Container Authentication in Swarm
@ -432,8 +440,6 @@ whoami:
- "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.
#### Rules Based Authentication

View File

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

View File

@ -16,7 +16,5 @@ spec:
services:
- name: traefik-forward-auth
port: 4181
middlewares:
- name: traefik-forward-auth
tls:
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-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 h1:Wz+5lgoB0kkuqLEc6NVmwRknTKP6dTGbSqvhZtBI/j0=
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=
@ -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/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 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
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=
@ -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/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 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.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
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/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 h1:GL2rEmy6nsikmW0r8opw9JIRScdMF5hA8cOYLH7In1k=
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=
@ -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/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 h1:aCvUg6QPl3ibpQUxyLkrEkCHtPqYJL4x9AuhqVqFis4=
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.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
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.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
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-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 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ=
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-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-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 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-20180909124046-d0be0721c37e/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-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/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-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 h1:5Beo0mZN8dRzgrMMkDp0jc8YXQKx9DiJ2k1dkvGsn5A=
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 h1:OB/uP/Puiu5vS5QMRPrXCDWUPb+kt8f1KW8oQzFejQw=
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=
@ -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.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 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
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=
@ -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/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 h1:p+PRuwXWwk5e+UYvicGiavEupapqM5NOxUl3y1GkD6c=
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/klog v0.3.1 h1:RVgyDHY/kFKtLqh67NvEWIgkMneNoIrdkN0CxDSQc68=
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=

View File

@ -59,18 +59,28 @@ func ValidateCookie(r *http.Request, c *http.Cookie) (string, error) {
// ValidateEmail checks if the given email address matches either a whitelisted
// email address, as defined by the "whitelist" config parameter. Or is part of
// 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?
if len(config.Whitelist) == 0 && len(config.Domains) == 0 {
if len(whitelist) == 0 && len(domains) == 0 {
return true
}
// Email whitelist validation
if len(config.Whitelist) > 0 {
for _, whitelist := range config.Whitelist {
if email == whitelist {
return true
}
if len(whitelist) > 0 {
if ValidateWhitelist(email, whitelist) {
return true
}
// If we're not matching *either*, stop here
@ -80,43 +90,54 @@ func ValidateEmail(email string) bool {
}
// Domain validation
if len(config.Domains) > 0 {
parts := strings.Split(email, "@")
if len(parts) < 2 {
return false
}
for _, domain := range config.Domains {
if domain == parts[1] {
return true
}
}
if len(domains) > 0 && ValidateDomains(email, domains) {
return true
}
return false
}
// ValidateWhitelist checks if the email is in whitelist
func ValidateWhitelist(email string, whitelist CommaSeparatedList) bool {
for _, whitelist := range whitelist {
if email == whitelist {
return true
}
}
return false
}
// ValidateDomains checks if the email matches a whitelisted domain
func ValidateDomains(email string, domains CommaSeparatedList) bool {
parts := strings.Split(email, "@")
if len(parts) < 2 {
return false
}
for _, domain := range domains {
if domain == parts[1] {
return true
}
}
return false
}
// Utility methods
// Get the redirect base
func redirectBase(r *http.Request) string {
proto := r.Header.Get("X-Forwarded-Proto")
host := r.Header.Get("X-Forwarded-Host")
return fmt.Sprintf("%s://%s", proto, host)
return fmt.Sprintf("%s://%s", r.Header.Get("X-Forwarded-Proto"), r.Host)
}
// Return url
func returnUrl(r *http.Request) string {
path := r.Header.Get("X-Forwarded-Uri")
return fmt.Sprintf("%s%s", redirectBase(r), path)
return fmt.Sprintf("%s%s", redirectBase(r), r.URL.Path)
}
// Get oauth redirect uri
func redirectUri(r *http.Request) string {
if use, _ := useAuthDomain(r); use {
proto := r.Header.Get("X-Forwarded-Proto")
return fmt.Sprintf("%s://%s%s", proto, config.AuthHost, config.Path)
p := r.Header.Get("X-Forwarded-Proto")
return fmt.Sprintf("%s://%s%s", p, config.AuthHost, 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?
reqMatch, reqHost := matchCookieDomains(r.Header.Get("X-Forwarded-Host"))
reqMatch, reqHost := matchCookieDomains(r.Host)
// Do any of the auth hosts match a cookie domain?
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)
//
// 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 {
return &http.Cookie{
Name: config.CSRFCookieName,
Name: buildCSRFCookieName(nonce),
Value: nonce,
Path: "/",
Domain: csrfCookieDomain(r),
HttpOnly: true,
Secure: !config.InsecureCookie,
Expires: cookieExpiry(),
Expires: time.Now().Local().Add(time.Hour * 1),
}
}
// 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{
Name: config.CSRFCookieName,
Name: c.Name,
Value: "",
Path: "/",
Domain: csrfCookieDomain(r),
@ -196,18 +225,18 @@ func ClearCSRFCookie(r *http.Request) *http.Cookie {
}
}
// ValidateCSRFCookie validates the csrf cookie against state
func ValidateCSRFCookie(r *http.Request, c *http.Cookie) (valid bool, provider string, redirect string, err error) {
state := r.URL.Query().Get("state")
// FindCSRFCookie extracts the CSRF cookie from the request based on state.
func FindCSRFCookie(r *http.Request, state string) (c *http.Cookie, err error) {
// 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 {
return false, "", "", errors.New("Invalid CSRF cookie value")
}
if len(state) < 34 {
return false, "", "", errors.New("Invalid CSRF state value")
}
// Check nonce match
if c.Value != state[:32] {
return false, "", "", errors.New("CSRF cookie does not match state")
@ -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))
}
// 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) {
nonce := make([]byte, 16)
@ -242,10 +279,8 @@ func Nonce() (error, string) {
// Cookie domain
func cookieDomain(r *http.Request) string {
host := r.Header.Get("X-Forwarded-Host")
// Check if any of the given cookie domains matches
_, domain := matchCookieDomains(host)
_, domain := matchCookieDomains(r.Host)
return domain
}
@ -255,7 +290,7 @@ func csrfCookieDomain(r *http.Request) string {
if use, domain := useAuthDomain(r); use {
host = domain
} else {
host = r.Header.Get("X-Forwarded-Host")
host = r.Host
}
// Remove port

View File

@ -1,8 +1,8 @@
package tfa
import (
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
@ -66,32 +66,25 @@ func TestAuthValidateEmail(t *testing.T) {
assert := assert.New(t)
config, _ = NewConfig([]string{})
// Should allow any
v := ValidateEmail("test@test.com")
// Should allow any with no whitelist/domain is specified
v := ValidateEmail("test@test.com", "default")
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")
// 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
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")
// 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
config.Domains = []string{}
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
@ -99,33 +92,113 @@ func TestAuthValidateEmail(t *testing.T) {
config.Domains = []string{"example.com"}
config.Whitelist = []string{"test@test.com"}
config.MatchWhitelistOrDomain = false
v = ValidateEmail("test@test.com")
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")
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("test@test.com")
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")
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")
}
func TestRedirectUri(t *testing.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-Host", "app.example.com")
r.Header.Add("X-Forwarded-Uri", "/hello")
//
// No Auth Host
@ -167,10 +240,8 @@ func TestRedirectUri(t *testing.T) {
// With Auth URL + cookie domain, but from different domain
// - 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-Host", "another.com")
r.Header.Add("X-Forwarded-Uri", "/hello")
config.AuthHost = "auth.example.com"
config.CookieDomains = []CookieDomain{*NewCookieDomain("example.com")}
@ -217,29 +288,30 @@ func TestAuthMakeCSRFCookie(t *testing.T) {
// No cookie domain or auth url
c := MakeCSRFCookie(r, "12345678901234567890123456789012")
assert.Equal("_forward_auth_csrf_123456", c.Name)
assert.Equal("app.example.com", c.Domain)
// With cookie domain but no auth url
config = &Config{
CookieDomains: []CookieDomain{*NewCookieDomain("example.com")},
}
c = MakeCSRFCookie(r, "12345678901234567890123456789012")
config.CookieDomains = []CookieDomain{*NewCookieDomain("example.com")}
c = MakeCSRFCookie(r, "12222278901234567890123456789012")
assert.Equal("_forward_auth_csrf_122222", c.Name)
assert.Equal("app.example.com", c.Domain)
// With cookie domain and auth url
config = &Config{
AuthHost: "auth.example.com",
CookieDomains: []CookieDomain{*NewCookieDomain("example.com")},
}
c = MakeCSRFCookie(r, "12345678901234567890123456789012")
config.AuthHost = "auth.example.com"
config.CookieDomains = []CookieDomain{*NewCookieDomain("example.com")}
c = MakeCSRFCookie(r, "12333378901234567890123456789012")
assert.Equal("_forward_auth_csrf_123333", c.Name)
assert.Equal("example.com", c.Domain)
}
func TestAuthClearCSRFCookie(t *testing.T) {
assert := assert.New(t)
config, _ = NewConfig([]string{})
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 != "" {
t.Error("ClearCSRFCookie should create cookie with empty value")
}
@ -249,63 +321,62 @@ func TestAuthValidateCSRFCookie(t *testing.T) {
assert := assert.New(t)
config, _ = NewConfig([]string{})
c := &http.Cookie{}
newCsrfRequest := func(state string) *http.Request {
u := fmt.Sprintf("http://example.com?state=%s", state)
r, _ := http.NewRequest("GET", u, nil)
return r
}
state := ""
// Should require 32 char string
r := newCsrfRequest("")
state = ""
c.Value = ""
valid, _, _, err := ValidateCSRFCookie(r, c)
valid, _, _, err := ValidateCSRFCookie(c, state)
assert.False(valid)
if assert.Error(err) {
assert.Equal("Invalid CSRF cookie value", err.Error())
}
c.Value = "123456789012345678901234567890123"
valid, _, _, err = ValidateCSRFCookie(r, c)
valid, _, _, err = ValidateCSRFCookie(c, state)
assert.False(valid)
if assert.Error(err) {
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
r = newCsrfRequest("12345678901234567890123456789012:99")
state = "12345678901234567890123456789012:99"
c.Value = "12345678901234567890123456789012"
valid, _, _, err = ValidateCSRFCookie(r, c)
valid, _, _, err = ValidateCSRFCookie(c, state)
assert.False(valid)
if assert.Error(err) {
assert.Equal("Invalid CSRF state format", err.Error())
}
// Should allow valid state
r = newCsrfRequest("12345678901234567890123456789012:p99:url123")
state = "12345678901234567890123456789012:p99:url123"
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.Nil(err, "valid request should not return an error")
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, _ := 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-Host", "example.com")
r.Header.Add("X-Forwarded-Uri", "/hello")
// Test with 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"`
SecretString string `long:"secret" env:"SECRET" description:"Secret used for signing (required)" json:"-"`
Whitelist CommaSeparatedList `long:"whitelist" env:"WHITELIST" env-delim:"," description:"Only allow given email addresses, can be set multiple times"`
Port int `long:"port" env:"PORT" default:"4181" description:"Port to listen on"`
Providers provider.Providers `group:"providers" namespace:"providers" env-namespace:"PROVIDERS"`
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:"-"`
Lifetime time.Duration
// Authorization
RequiredRole string `long:"required-role" env:"REQUIRED_ROLE" description:"Required role to verify authorization"`
// Legacy
CookieDomainsLegacy CookieDomains `long:"cookie-domains" env:"COOKIE_DOMAINS" description:"DEPRECATED - Use \"cookie-domain\""`
CookieSecretLegacy string `long:"cookie-secret" env:"COOKIE_SECRET" description:"DEPRECATED - Use \"secret\"" json:"-"`
@ -210,6 +214,14 @@ func (c *Config) parseUnknownFlag(option string, arg flags.SplitArgument, args [
rule.Rule = val
case "provider":
rule.Provider = val
case "whitelist":
list := CommaSeparatedList{}
list.UnmarshalFlag(val)
rule.Whitelist = list
case "domains":
list := CommaSeparatedList{}
list.UnmarshalFlag(val)
rule.Domains = list
default:
return args, fmt.Errorf("invalid route param: %v", option)
}
@ -317,7 +329,7 @@ func (c *Config) setupProvider(name string) error {
}
// Setup
err = p.Setup()
err = p.Setup(log)
if err != nil {
return err
}
@ -327,9 +339,11 @@ func (c *Config) setupProvider(name string) error {
// Rule holds defined rules
type Rule struct {
Action string
Rule string
Provider string
Action string
Rule string
Provider string
Whitelist CommaSeparatedList
Domains CommaSeparatedList
}
// NewRule creates a new rule object

View File

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

View File

@ -8,6 +8,8 @@ import (
"net/http"
"golang.org/x/oauth2"
"github.com/sirupsen/logrus"
)
// GenericOAuth provider
@ -29,7 +31,7 @@ func (o *GenericOAuth) Name() string {
}
// Setup performs validation and setup
func (o *GenericOAuth) Setup() error {
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")
@ -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
func (o *GenericOAuth) GetUser(token string) (User, error) {
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, err
return user, roles, err
}
if o.TokenStyle == "header" {
@ -86,11 +89,11 @@ func (o *GenericOAuth) GetUser(token string) (User, error) {
client := &http.Client{}
res, err := client.Do(req)
if err != nil {
return user, err
return user, roles, err
}
defer res.Body.Close()
err = json.NewDecoder(res.Body).Decode(&user)
return user, err
return user, roles, err
}

View File

@ -6,6 +6,8 @@ import (
"fmt"
"net/http"
"net/url"
"github.com/sirupsen/logrus"
)
// Google provider
@ -26,7 +28,7 @@ func (g *Google) Name() string {
}
// Setup performs validation and setup
func (g *Google) Setup() error {
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")
}
@ -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
func (g *Google) GetUser(token string) (User, error) {
func (g *Google) GetUser(token string) (User, Roles, error) {
var user User
var roles Roles
client := &http.Client{}
req, err := http.NewRequest("GET", g.UserURL.String(), nil)
if err != nil {
return user, err
return user, roles, err
}
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
res, err := client.Do(req)
if err != nil {
return user, err
return user, roles, err
}
defer res.Body.Close()
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"
"golang.org/x/oauth2"
"github.com/sirupsen/logrus"
)
// OIDC provider
@ -18,6 +20,8 @@ type OIDC struct {
provider *oidc.Provider
verifier *oidc.IDTokenVerifier
log *logrus.Logger
}
// Name returns the name of the provider
@ -26,7 +30,9 @@ func (o *OIDC) Name() string {
}
// Setup performs validation and setup
func (o *OIDC) Setup() error {
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")
@ -70,6 +76,7 @@ func (o *OIDC) ExchangeCode(redirectURI, code string) (string, error) {
if err != nil {
return "", err
}
o.log.WithField("accessToken", token.AccessToken).Debug("getUser")
// Extract ID token
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
func (o *OIDC) GetUser(token string) (User, error) {
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, err
return user, roles, err
}
// Extract custom claims
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"
"golang.org/x/oauth2"
"github.com/sirupsen/logrus"
)
// Providers contains all the implemented providers
@ -19,8 +20,8 @@ type Provider interface {
Name() string
GetLoginURL(redirectURI, state string) string
ExchangeCode(redirectURI, code string) (string, error)
GetUser(token string) (User, error)
Setup() error
GetUser(token string) (User, Roles, error)
Setup(*logrus.Logger) error
}
type token struct {
@ -32,6 +33,10 @@ type User struct {
Email string `json:"email"`
}
type Roles struct {
Roles []string `json:"roles"`
}
// OAuthProvider is a provider using the oauth2 library
type OAuthProvider struct {
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
r.Method = r.Header.Get("X-Forwarded-Method")
r.Host = r.Header.Get("X-Forwarded-Host")
r.URL, _ = url.Parse(r.Header.Get("X-Forwarded-Uri"))
// Read URI from header if we're acting as forward auth middleware
if _, ok := r.Header["X-Forwarded-Uri"]; ok {
r.URL, _ = url.Parse(r.Header.Get("X-Forwarded-Uri"))
}
// Pass to mux
s.router.ServeHTTP(w, r)
@ -101,7 +105,7 @@ func (s *Server) AuthHandler(providerName, rule string) http.HandlerFunc {
}
// Validate user
valid := ValidateEmail(email)
valid := ValidateEmail(email, rule)
if !valid {
logger.WithField("email", email).Warn("Invalid email")
http.Error(w, "Not authorized", 401)
@ -121,16 +125,26 @@ func (s *Server) AuthCallbackHandler() http.HandlerFunc {
// Logging setup
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
c, err := r.Cookie(config.CSRFCookieName)
c, err := FindCSRFCookie(r, state)
if err != nil {
logger.Info("Missing csrf cookie")
http.Error(w, "Not authorized", 401)
return
}
// Validate state
valid, providerName, redirect, err := ValidateCSRFCookie(r, c)
// Validate CSRF cookie against state
valid, providerName, redirect, err := ValidateCSRFCookie(c, state)
if !valid {
logger.WithFields(logrus.Fields{
"error": err,
@ -153,7 +167,7 @@ func (s *Server) AuthCallbackHandler() http.HandlerFunc {
}
// Clear CSRF cookie
http.SetCookie(w, ClearCSRFCookie(r))
http.SetCookie(w, ClearCSRFCookie(r, c))
// Exchange code for token
token, err := p.ExchangeCode(redirectUri(r), r.URL.Query().Get("code"))
@ -164,12 +178,23 @@ func (s *Server) AuthCallbackHandler() http.HandlerFunc {
}
// Get user
user, err := p.GetUser(token)
user, roles, err := p.GetUser(token)
if err != nil {
logger.WithField("error", err).Error("Error getting user")
http.Error(w, "Service unavailable", 503)
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
http.SetCookie(w, MakeCookie(r, user.Email))
@ -177,6 +202,7 @@ func (s *Server) AuthCallbackHandler() http.HandlerFunc {
"provider": providerName,
"redirect": redirect,
"user": user.Email,
"roles": roles.Roles,
}).Info("Successfully generated auth cookie, redirecting user.")
// Redirect

View File

@ -31,6 +31,37 @@ func init() {
* 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) {
assert := assert.New(t)
config = newDefaultConfig()
@ -90,15 +121,15 @@ func TestServerAuthHandlerExpired(t *testing.T) {
config.Domains = []string{"test.com"}
// Should redirect expired cookie
req := newDefaultHttpRequest("/foo")
req := newHTTPRequest("GET", "http://example.com/foo")
c := MakeCookie(req, "test@example.com")
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
var cookie *http.Cookie
for _, c := range res.Cookies() {
if c.Name == config.CSRFCookieName {
if strings.HasPrefix(c.Name, config.CSRFCookieName) {
cookie = c
}
}
@ -116,7 +147,7 @@ func TestServerAuthHandlerValid(t *testing.T) {
config = newDefaultConfig()
// Should allow valid request email
req := newDefaultHttpRequest("/foo")
req := newHTTPRequest("GET", "http://example.com/foo")
c := MakeCookie(req, "test@example.com")
config.Domains = []string{}
@ -131,6 +162,7 @@ func TestServerAuthHandlerValid(t *testing.T) {
func TestServerAuthCallback(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
config = newDefaultConfig()
// Setup OAuth server
@ -148,27 +180,28 @@ func TestServerAuthCallback(t *testing.T) {
}
// Should pass auth response request to callback
req := newDefaultHttpRequest("/_oauth")
req := newHTTPRequest("GET", "http://example.com/_oauth")
res, _ := doHttpRequest(req, nil)
assert.Equal(401, res.StatusCode, "auth callback without cookie shouldn't be authorised")
// 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")
res, _ = doHttpRequest(req, c)
assert.Equal(401, res.StatusCode, "auth callback with invalid cookie shouldn't be authorised")
// Should catch invalid provider cookie
req = newDefaultHttpRequest("/_oauth?state=12345678901234567890123456789012:invalid:http://redirect")
c = MakeCSRFCookie(req, "12345678901234567890123456789012")
req = newHTTPRequest("GET", "http://example.com/_oauth?state="+nonce+":invalid:http://redirect")
c = MakeCSRFCookie(req, nonce)
res, _ = doHttpRequest(req, c)
assert.Equal(401, res.StatusCode, "auth callback with invalid provider shouldn't be authorised")
// Should redirect valid request
req = newDefaultHttpRequest("/_oauth?state=12345678901234567890123456789012:google:http://redirect")
c = MakeCSRFCookie(req, "12345678901234567890123456789012")
req = newHTTPRequest("GET", "http://example.com/_oauth?state="+nonce+":google:http://redirect")
c = MakeCSRFCookie(req, nonce)
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()
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
req := newHttpRequest("GET", "https://example.com/", "/")
req := newHTTPRequest("GET", "https://example.com/")
res, _ := doHttpRequest(req, nil)
assert.Equal(307, res.StatusCode, "request not matching any rule should require auth")
// Should allow matching request
req = newHttpRequest("GET", "https://api.example.com/", "/")
req = newHTTPRequest("GET", "https://api.example.com/")
res, _ = doHttpRequest(req, nil)
assert.Equal(200, res.StatusCode, "request matching allow rule should be allowed")
// Should allow matching request
req = newHttpRequest("GET", "https://sub8.example.com/", "/")
req = newHTTPRequest("GET", "https://sub8.example.com/")
res, _ = doHttpRequest(req, nil)
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
req := newHttpRequest("GET", "https://example.com/", "/")
req := newHTTPRequest("GET", "https://example.com/")
res, _ := doHttpRequest(req, nil)
assert.Equal(307, res.StatusCode, "request not matching any rule should require auth")
// Should allow matching request
req = newHttpRequest("PUT", "https://example.com/", "/")
req = newHTTPRequest("PUT", "https://example.com/")
res, _ = doHttpRequest(req, nil)
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
req := newHttpRequest("GET", "https://example.com/", "/?q=no")
req := newHTTPRequest("GET", "https://example.com/?q=no")
res, _ := doHttpRequest(req, nil)
assert.Equal(307, res.StatusCode, "request not matching any rule should require auth")
// Should allow matching request
req = newHttpRequest("GET", "https://api.example.com/", "/?q=test123")
req = newHTTPRequest("GET", "https://api.example.com/?q=test123")
res, _ = doHttpRequest(req, nil)
assert.Equal(200, res.StatusCode, "request matching allow rule should be allowed")
}
@ -531,16 +564,17 @@ func newDefaultConfig() *Config {
return config
}
// TODO: replace with newHTTPRequest("GET", "http://example.com/"+uri)
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 {
r := httptest.NewRequest("", "http://should-use-x-forwarded.com", nil)
p, _ := url.Parse(dest)
func newHTTPRequest(method, target string) *http.Request {
u, _ := url.Parse(target)
r := httptest.NewRequest(method, target, nil)
r.Header.Add("X-Forwarded-Method", method)
r.Header.Add("X-Forwarded-Proto", p.Scheme)
r.Header.Add("X-Forwarded-Host", p.Host)
r.Header.Add("X-Forwarded-Uri", uri)
r.Header.Add("X-Forwarded-Proto", u.Scheme)
r.Header.Add("X-Forwarded-Host", u.Host)
r.Header.Add("X-Forwarded-Uri", u.RequestURI())
return r
}