Compare commits
16 Commits
Author | SHA1 | Date | |
---|---|---|---|
e41e8e1a17
|
|||
86894f72ed
|
|||
3f9d70e87b
|
|||
fb24320552
|
|||
f6120640d2
|
|||
ab2d527dbd
|
|||
5828a9a5a2
|
|||
c4317b7503 | |||
4ffb6593d5 | |||
6c6f75e80d | |||
8be8244b13 | |||
f96a3fb332 | |||
c19f622fbd | |||
04f5499f0b | |||
41560feaa7 | |||
1743537438 |
@ -1,3 +1,4 @@
|
||||
example
|
||||
.travis.yml
|
||||
.git
|
||||
.gitlab-ci.yml
|
||||
|
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
@ -1,2 +0,0 @@
|
||||
github: thomseddon
|
||||
|
6
.gitlab-ci.yml
Normal file
6
.gitlab-ci.yml
Normal file
@ -0,0 +1,6 @@
|
||||
include:
|
||||
- project: dockerized/commons
|
||||
ref: master
|
||||
file: gitlab-ci-template.yml
|
||||
|
||||
|
@ -1,5 +0,0 @@
|
||||
language: go
|
||||
sudo: false
|
||||
go:
|
||||
- "1.12"
|
||||
script: env GO111MODULE=on go test -v ./...
|
@ -1,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"]
|
@ -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"]
|
@ -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
|
||||
|
@ -1,5 +1,5 @@
|
||||
|
||||
# Traefik Forward Auth [](https://travis-ci.org/thomseddon/traefik-forward-auth) [](https://goreportcard.com/report/github.com/thomseddon/traefik-forward-auth)  [](https://GitHub.com/thomseddon/traefik-forward-auth/releases/)
|
||||
# Traefik Forward Auth  [](https://goreportcard.com/report/github.com/thomseddon/traefik-forward-auth)  [](https://GitHub.com/thomseddon/traefik-forward-auth/releases/)
|
||||
|
||||
|
||||
A minimal forward authentication service that provides OAuth/SSO login and authentication for the [traefik](https://github.com/containous/traefik) reverse proxy/load balancer.
|
||||
@ -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
|
@ -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))
|
||||
}
|
||||
|
@ -16,7 +16,5 @@ spec:
|
||||
services:
|
||||
- name: traefik-forward-auth
|
||||
port: 4181
|
||||
middlewares:
|
||||
- name: traefik-forward-auth
|
||||
tls:
|
||||
certresolver: default
|
||||
|
15
go.sum
15
go.sum
@ -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=
|
||||
|
119
internal/auth.go
119
internal/auth.go
@ -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
|
||||
|
@ -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{}
|
||||
|
@ -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
|
||||
|
@ -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{
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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"`
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
Reference in New Issue
Block a user