Skip to content

Commit e0303f5

Browse files
committedMay 18, 2020
Rewriting the universe
0 parents  commit e0303f5

File tree

22 files changed

+2323
-0
lines changed

22 files changed

+2323
-0
lines changed
 

‎.github/workflows/main.yml

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
on: [push, pull_request]
2+
name: testify
3+
jobs:
4+
lint:
5+
runs-on: ubuntu-latest
6+
steps:
7+
- uses: actions/checkout@v2
8+
- name: golangci-lint
9+
uses: golangci/golangci-lint-action@v1
10+
with:
11+
# Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version.
12+
version: v1.26
13+
args: -E bodyclose,misspell,gocyclo,dupl,gofmt,golint,unconvert,goimports,depguard,gocritic,funlen,interfacer
14+
test:
15+
strategy:
16+
matrix:
17+
go-version: [1.14.x]
18+
platform: [ubuntu-latest, macos-latest, windows-latest]
19+
runs-on: ${{ matrix.platform }}
20+
steps:
21+
- name: Install Go
22+
if: success()
23+
uses: actions/setup-go@v1
24+
with:
25+
go-version: ${{ matrix.go-version }}
26+
- name: Checkout code
27+
uses: actions/checkout@v1
28+
- name: Run tests
29+
run: go test -v -covermode=count
30+
31+
# coverage:
32+
# runs-on: ubuntu-latest
33+
# steps:
34+
# - name: Install Go
35+
# if: success()
36+
# uses: actions/setup-go@v1
37+
# with:
38+
# go-version: 1.14.x
39+
# - name: Checkout code
40+
# uses: actions/checkout@v1
41+
# - name: Calc coverage
42+
# run: |
43+
# export PATH=$PATH:$(go env GOPATH)/bin
44+
# go test -v -covermode=count -coverprofile=coverage.out
45+
# - name: Convert coverage to lcov
46+
# uses: jandelgado/gcov2lcov-action@v1.0.0
47+
# with:
48+
# infile: coverage.out
49+
# outfile: coverage.lcov
50+
# - name: Coveralls
51+
# uses: coverallsapp/github-action@v1.0.1
52+
# with:
53+
# github-token: ${{ secrets.github_token }}
54+
# path-to-lcov: coverage.lcov

‎.gitignore

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
vendor/**
2+
bin/**
3+
.coverage/**
4+
.doc/**
5+
*TODO*
6+
123*
7+
8+
# Binaries for programs and plugins
9+
*.exe
10+
*.exe~
11+
*.dll
12+
*.so
13+
*.dylib
14+
15+
# Test binary, built with `go test -c`
16+
*.test
17+
18+
# Output of the go coverage tool, specifically when used with LiteIDE
19+
*.out
20+
21+
# Dependency directories (remove the comment below to include it)
22+
# vendor/

‎LICENSE

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
The MIT License (MIT)
2+
3+
Copyright (c) 2020 Shannon Wynter
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

‎README.md

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# caddy-auth-forms
2+
3+
<a href="https://github.com/freman/caddy2-reauth/actions/" target="_blank">![testify](https://github.com/freman/caddy2-reauth/workflows/testify/badge.svg?branch=master)</a>
4+
<a href="https://pkg.go.dev/github.com/freman/caddy2-reauth" target="_blank"><img src="https://img.shields.io/badge/godoc-reference-blue.svg"></a>
5+
<a href="https://caddy.community" target="_blank"><img src="https://img.shields.io/badge/community-forum-ff69b4.svg"></a>
6+
7+
Another authentication plugin for [Caddy v2](https://github.com/caddyserver/caddy).
8+
9+
## TODO
10+
11+
* Tests
12+
* Examples
13+
* Readme

‎assets/conf/Caddyfile.json

+85
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
{
2+
"logging": {
3+
"logs": {
4+
"default": {
5+
"level": "DEBUG"
6+
}
7+
}
8+
},
9+
"apps": {
10+
"http": {
11+
"http_port": 9080,
12+
"servers": {
13+
"srv0": {
14+
"listen": [
15+
":9080"
16+
],
17+
"routes": [
18+
{
19+
"match": [
20+
{
21+
"path": [
22+
"/"
23+
]
24+
}
25+
],
26+
"handle": [
27+
{
28+
"handler": "static_response",
29+
"status_code": 200,
30+
"body": "hello world"
31+
}
32+
],
33+
"terminal": true
34+
},
35+
{
36+
"handle": [
37+
{
38+
"handler": "authentication",
39+
"providers": {
40+
"reauth": {
41+
"backends": [
42+
{
43+
"type": "simple",
44+
"credentials": {
45+
"username": "password"
46+
}
47+
}
48+
],
49+
"failure": {
50+
"mode": "status",
51+
"code": 403
52+
}
53+
}
54+
}
55+
},
56+
{
57+
"handler": "static_response",
58+
"status_code": 200,
59+
"body": "tell no-one"
60+
}
61+
],
62+
"match": [
63+
{
64+
"path": [
65+
"/secret"
66+
]
67+
}
68+
],
69+
"terminal": true
70+
}
71+
],
72+
"tls_connection_policies": [
73+
{
74+
"certificate_selection": {
75+
"any_tag": [
76+
"cert0"
77+
]
78+
}
79+
}
80+
]
81+
}
82+
}
83+
}
84+
}
85+
}

‎backend.go

+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package reauth
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"net/http"
7+
8+
"github.com/freman/caddy2-reauth/backends"
9+
"github.com/freman/caddy2-reauth/backends/gitlabci"
10+
"github.com/freman/caddy2-reauth/backends/ldap"
11+
"github.com/freman/caddy2-reauth/backends/simple"
12+
"github.com/freman/caddy2-reauth/backends/upstream"
13+
)
14+
15+
// Backend is an authentication backend.
16+
type Backend struct {
17+
Type string `json:"type,omitempty"`
18+
driver backends.Driver
19+
}
20+
21+
// Authenticate performs authentication with an authentication provider.
22+
func (b *Backend) Authenticate(r *http.Request) (string, error) {
23+
return b.driver.Authenticate(r)
24+
}
25+
26+
// Validate checks whether an authentication provider is functional.
27+
func (b *Backend) Validate() error {
28+
return b.driver.Validate()
29+
}
30+
31+
// MarshalJSON packs configuration info JSON byte array
32+
func (b Backend) MarshalJSON() ([]byte, error) {
33+
return json.Marshal(b.driver)
34+
}
35+
36+
// UnmarshalJSON unpacks configuration into appropriate structures.
37+
func (b *Backend) UnmarshalJSON(data []byte) error {
38+
if len(data) < 10 {
39+
return fmt.Errorf("invalid configuration: %s", data)
40+
}
41+
42+
type undecorated Backend
43+
var backend undecorated
44+
45+
if err := json.Unmarshal(data, &backend); err != nil {
46+
return fmt.Errorf("invalid reauth configuration, error: %s, config: %s", err, data)
47+
}
48+
49+
var driver backends.Driver
50+
switch backend.Type {
51+
case gitlabci.BackendName:
52+
driver = gitlabci.NewDriver()
53+
case ldap.BackendName:
54+
driver = ldap.NewDriver()
55+
case simple.BackendName:
56+
driver = simple.NewDriver()
57+
case upstream.BackendName:
58+
driver = upstream.NewDriver()
59+
default:
60+
return fmt.Errorf("invalid reauth configuration, error: unknown backend %q, config: %s", backend.Type, data)
61+
}
62+
63+
if err := json.Unmarshal(data, driver); err != nil {
64+
return fmt.Errorf("invalid reauth:%s configuration, error: %s, config:%s", backend.Type, err, data)
65+
}
66+
67+
if err := driver.Validate(); err != nil {
68+
return fmt.Errorf("invalid reauth:%s configuration, error: %s, config: %s", backend.Type, err, data)
69+
}
70+
71+
b.Type = backend.Type
72+
b.driver = driver
73+
74+
return nil
75+
}

‎backends/driver.go

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package backends
2+
3+
import (
4+
"net/http"
5+
)
6+
7+
// Driver is an interface to an authentication provider.
8+
type Driver interface {
9+
Authenticate(r *http.Request) (string, error)
10+
Validate() error
11+
}

‎backends/gitlabci/auth.go

+134
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
/*
2+
* The MIT License (MIT)
3+
*
4+
* Copyright (c) 2017 Shannon Wynter
5+
*
6+
* Permission is hereby granted, free of charge, to any person obtaining a copy
7+
* of this software and associated documentation files (the "Software"), to deal
8+
* in the Software without restriction, including without limitation the rights
9+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
* copies of the Software, and to permit persons to whom the Software is
11+
* furnished to do so, subject to the following conditions:
12+
*
13+
* The above copyright notice and this permission notice shall be included in all
14+
* copies or substantial portions of the Software.
15+
*
16+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22+
* SOFTWARE.
23+
*/
24+
25+
package gitlabci
26+
27+
import (
28+
"crypto/tls"
29+
"errors"
30+
"fmt"
31+
"net/http"
32+
"time"
33+
34+
"github.com/freman/caddy2-reauth/backends"
35+
"github.com/freman/caddy2-reauth/jsontypes"
36+
)
37+
38+
// Interface guard
39+
var _ backends.Driver = (*GitlabCI)(nil)
40+
41+
// BackendName name
42+
const BackendName = "gitlabci"
43+
44+
const defaultTimeout = time.Minute
45+
const defaultUsername = "gitlab-ci-token"
46+
47+
// GitlabCI backend provides authentication against gitlab paths, primarily to make
48+
// it easier to dynamically authenticate the gitlab-ci against gitlab permitting
49+
// testing access to otherwise private resources without storing credentials in
50+
// gitlab or gitlab-ci.yml
51+
//
52+
// Authenticating against this backend should be done with the project path as
53+
// the username and the token as the password.
54+
//
55+
// Example: docker login docker.example.com -u "$CI_PROJECT_PATH" -p "$CI_BUILD_TOKEN"
56+
type GitlabCI struct {
57+
URL *jsontypes.URL `json:"url,omitempty"`
58+
Timeout jsontypes.Duration `json:"timeout,omitempty"`
59+
Username string `json:"username,omitempty"`
60+
InsecureSkipVerify bool `json:"insecure_skip_verify,omitempty"`
61+
}
62+
63+
// NewDriver returns a GitlabCI instance with some defaults
64+
func NewDriver() *GitlabCI {
65+
return &GitlabCI{
66+
Timeout: jsontypes.Duration{Duration: defaultTimeout},
67+
Username: defaultUsername,
68+
}
69+
}
70+
71+
// Validate that this module is ready to go
72+
func (h GitlabCI) Validate() error {
73+
if h.Username == "" {
74+
return errors.New("username is a required option")
75+
}
76+
77+
if h.Timeout.Duration <= 0 {
78+
return errors.New("timeout must be greater than 0")
79+
}
80+
81+
if h.URL == nil {
82+
return errors.New("url to auth against is a required parameter")
83+
}
84+
85+
return nil
86+
}
87+
88+
func noRedirectsPolicy(req *http.Request, via []*http.Request) error {
89+
return errors.New("follow redirects disabled")
90+
}
91+
92+
// Authenticate fulfils the backend interface
93+
func (h GitlabCI) Authenticate(r *http.Request) (string, error) {
94+
un, pw, k := r.BasicAuth()
95+
if !k {
96+
return "", nil
97+
}
98+
99+
repo, err := h.URL.Parse(un + ".git/info/refs?service=git-upload-pack")
100+
if err != nil {
101+
return "", fmt.Errorf("unable to parse repo path: %v", err)
102+
}
103+
104+
c := &http.Client{
105+
Timeout: h.Timeout.Duration,
106+
CheckRedirect: noRedirectsPolicy,
107+
}
108+
109+
if repo.Scheme == "https" && h.InsecureSkipVerify {
110+
c.Transport = &http.Transport{
111+
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
112+
}
113+
}
114+
115+
req, err := http.NewRequest("GET", repo.String(), nil)
116+
if err != nil {
117+
return "", err
118+
}
119+
120+
req.SetBasicAuth(h.Username, pw)
121+
122+
resp, err := c.Do(req)
123+
if err != nil {
124+
return "", err
125+
}
126+
127+
resp.Body.Close()
128+
129+
if resp.StatusCode != 200 {
130+
return "", fmt.Errorf("unexpected status code from gitlabci: %d (%s)", resp.StatusCode, resp.Status)
131+
}
132+
133+
return un, nil
134+
}

‎backends/ldap/auth.go

+233
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
/*
2+
* The MIT License (MIT)
3+
*
4+
* Copyright (c) 2018 Tamás Gulácsi
5+
*
6+
* Permission is hereby granted, free of charge, to any person obtaining a copy
7+
* of this software and associated documentation files (the "Software"), to deal
8+
* in the Software without restriction, including without limitation the rights
9+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
* copies of the Software, and to permit persons to whom the Software is
11+
* furnished to do so, subject to the following conditions:
12+
*
13+
* The above copyright notice and this permission notice shall be included in all
14+
* copies or substantial portions of the Software.
15+
*
16+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22+
* SOFTWARE.
23+
*/
24+
25+
package ldap
26+
27+
import (
28+
"crypto/tls"
29+
"errors"
30+
"fmt"
31+
"net"
32+
"net/http"
33+
"strings"
34+
"time"
35+
36+
"github.com/freman/caddy2-reauth/backends"
37+
"github.com/freman/caddy2-reauth/jsontypes"
38+
39+
ldp "github.com/go-ldap/ldap/v3"
40+
)
41+
42+
// Interface guard
43+
var _ backends.Driver = (*LDAP)(nil)
44+
45+
// BackendName name
46+
const BackendName = "ldap"
47+
48+
const defaultPoolSize = 10
49+
const defaultTimeout = time.Minute
50+
const defaultFilter = "(&(objectClass=user)(sAMAccountName=%s))"
51+
52+
// LDAP backend provides authentication against LDAP paths, for example for Microsoft AD.
53+
type LDAP struct {
54+
URL *jsontypes.URL `json:"url,omitempty"`
55+
BaseDN string `json:"base_dn,omitempty"`
56+
FilterDN string `json:"filter_dn,omitempty"`
57+
PrincipalSuffix string `json:"principal_suffix,omitempty"`
58+
BindDN string `json:"bind_dn,omitempty"`
59+
BindPassword string `json:"bind_password,omitempty"`
60+
TLS bool `json:"tls,omitempty"`
61+
InsecureSkipVerify bool `json:"insecure_skip_verify,omitempty"`
62+
Timeout jsontypes.Duration `json:"timeout,omitempty"`
63+
ConnectionPoolSize int `json:"connection_pool_size,omitempty"`
64+
65+
pool chan ldp.Client
66+
}
67+
68+
// NewDriver returns a LDAP instance with some defaults
69+
func NewDriver() *LDAP {
70+
return &LDAP{
71+
Timeout: jsontypes.Duration{Duration: defaultTimeout},
72+
ConnectionPoolSize: defaultPoolSize,
73+
FilterDN: defaultFilter,
74+
}
75+
}
76+
77+
// Validate that this module is ready to go
78+
func (h *LDAP) Validate() error {
79+
var missing []string
80+
if h.URL == nil {
81+
missing = append(missing, "URL")
82+
}
83+
84+
if h.BindDN == "" {
85+
missing = append(missing, "BindDN")
86+
}
87+
88+
if h.BindPassword == "" {
89+
missing = append(missing, "BindPassword")
90+
}
91+
92+
if h.BaseDN == "" {
93+
missing = append(missing, "BaseDN")
94+
}
95+
96+
if n := len(missing); n > 0 {
97+
var s string
98+
if n > 1 {
99+
s = "s"
100+
}
101+
return errors.New("missing the following required parameter" + s + ": " + strings.Join(missing, ", "))
102+
}
103+
104+
if h.Timeout.Duration <= 0 {
105+
return errors.New("timeout must be greater than 0")
106+
}
107+
108+
if h.ConnectionPoolSize <= 0 {
109+
return errors.New("connection pool size must be greater than 0")
110+
}
111+
112+
h.pool = make(chan ldp.Client, h.ConnectionPoolSize)
113+
114+
c, err := h.getConnection()
115+
if err != nil {
116+
return err
117+
}
118+
h.stashConnection(c)
119+
120+
return nil
121+
}
122+
123+
// Authenticate fulfils the backend interface
124+
func (h *LDAP) Authenticate(r *http.Request) (string, error) {
125+
un, pw, k := r.BasicAuth()
126+
if !k {
127+
return "", nil
128+
}
129+
130+
c, err := h.getConnection()
131+
if err != nil {
132+
return "", err
133+
}
134+
defer h.stashConnection(c)
135+
136+
// Search for the given username
137+
searchRequest := ldp.NewSearchRequest(
138+
h.BaseDN,
139+
ldp.ScopeWholeSubtree, ldp.NeverDerefAliases, 0, int(h.Timeout.Duration/time.Second), false,
140+
fmt.Sprintf(h.FilterDN, un+h.PrincipalSuffix),
141+
[]string{"dn"},
142+
nil,
143+
)
144+
145+
sr, err := c.Search(searchRequest)
146+
if err != nil {
147+
return "", fmt.Errorf("search under %q for %q: %v", h.BaseDN, fmt.Sprintf(h.FilterDN, un+h.PrincipalSuffix), err)
148+
}
149+
150+
if len(sr.Entries) == 0 {
151+
return "", nil
152+
}
153+
154+
if len(sr.Entries) > 1 {
155+
return "", errors.New("too many entries returned")
156+
}
157+
158+
userDN := sr.Entries[0].DN
159+
160+
// Bind as the user to verify their password
161+
err = c.Bind(userDN, pw)
162+
if err != nil {
163+
if ldp.IsErrorWithCode(err, ldp.LDAPResultInvalidCredentials) {
164+
return "", nil
165+
}
166+
return "", fmt.Errorf("bind with %q: %v", userDN, err)
167+
}
168+
169+
return userDN, nil
170+
}
171+
172+
func (h *LDAP) getConnection() (ldp.Client, error) {
173+
var c ldp.Client
174+
select {
175+
case c = <-h.pool:
176+
if err := c.Bind(h.BindDN, h.BindPassword); err == nil {
177+
return c, nil
178+
}
179+
c.Close()
180+
default:
181+
}
182+
183+
host, port, _ := net.SplitHostPort(h.URL.Host)
184+
185+
ldaps := port == "636" || port == "3269" || h.URL.Scheme == "ldaps"
186+
if h.URL.Scheme == "ldap" {
187+
ldaps = false
188+
}
189+
if port == "" || port == "0" {
190+
port = "389"
191+
if ldaps {
192+
port = "636"
193+
}
194+
}
195+
196+
hostPort := fmt.Sprintf("%s:%s", host, port)
197+
198+
var err error
199+
if ldaps {
200+
c, err = ldp.DialTLS("tcp", hostPort, &tls.Config{InsecureSkipVerify: h.InsecureSkipVerify})
201+
} else {
202+
c, err = ldp.Dial("tcp", hostPort)
203+
}
204+
205+
if err != nil {
206+
return nil, fmt.Errorf("connect to %q: %v", hostPort, err)
207+
}
208+
209+
// Technically it's not impossible to run tls over ssl... just excessive
210+
if h.TLS {
211+
if err = c.StartTLS(&tls.Config{InsecureSkipVerify: h.InsecureSkipVerify}); err != nil {
212+
c.Close()
213+
return nil, fmt.Errorf("StartTLS: %v", err)
214+
}
215+
}
216+
217+
if err := c.Bind(h.BindDN, h.BindPassword); err != nil {
218+
c.Close()
219+
return nil, fmt.Errorf("bind with %q: %v", h.BindDN, err)
220+
}
221+
222+
return c, nil
223+
}
224+
225+
func (h *LDAP) stashConnection(c ldp.Client) {
226+
select {
227+
case h.pool <- c:
228+
return
229+
default:
230+
c.Close()
231+
return
232+
}
233+
}

‎backends/simple/auth.go

+80
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/*
2+
* The MIT License (MIT)
3+
*
4+
* Copyright (c) 2017 Shannon Wynter
5+
*
6+
* Permission is hereby granted, free of charge, to any person obtaining a copy
7+
* of this software and associated documentation files (the "Software"), to deal
8+
* in the Software without restriction, including without limitation the rights
9+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
* copies of the Software, and to permit persons to whom the Software is
11+
* furnished to do so, subject to the following conditions:
12+
*
13+
* The above copyright notice and this permission notice shall be included in all
14+
* copies or substantial portions of the Software.
15+
*
16+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22+
* SOFTWARE.
23+
*/
24+
25+
package simple
26+
27+
import (
28+
"net/http"
29+
30+
"github.com/freman/caddy2-reauth/backends"
31+
"golang.org/x/crypto/bcrypt"
32+
)
33+
34+
// Interface guard
35+
var _ backends.Driver = (*Simple)(nil)
36+
37+
// BackendName name
38+
const BackendName = "simple"
39+
40+
// Simple is the simplest backend for authentication, a name:password map
41+
type Simple struct {
42+
UseBcrypt bool `json:"use_bcrypt,omitempty"`
43+
Credentials map[string]string `json:"credentials,omitempty"`
44+
}
45+
46+
// NewDriver returns a new instance of Simple with some defaults
47+
func NewDriver() *Simple {
48+
return &Simple{
49+
Credentials: map[string]string{},
50+
}
51+
}
52+
53+
// Validate verifies that this module is functional with the given configuration
54+
func (h Simple) Validate() error {
55+
return nil
56+
}
57+
58+
// Authenticate fulfils the backend interface
59+
func (h Simple) Authenticate(r *http.Request) (string, error) {
60+
un, pw, k := r.BasicAuth()
61+
if !k {
62+
return "", nil
63+
}
64+
65+
if p, found := h.Credentials[un]; found {
66+
if h.UseBcrypt {
67+
if bcrypt.CompareHashAndPassword([]byte(p), []byte(pw)) == nil {
68+
return un, nil
69+
}
70+
71+
return "", nil
72+
}
73+
74+
if p == pw {
75+
return un, nil
76+
}
77+
}
78+
79+
return "", nil
80+
}

‎backends/upstream/auth.go

+162
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
/*
2+
* The MIT License (MIT)
3+
*
4+
* Copyright (c) 2017 Shannon Wynter
5+
*
6+
* Permission is hereby granted, free of charge, to any person obtaining a copy
7+
* of this software and associated documentation files (the "Software"), to deal
8+
* in the Software without restriction, including without limitation the rights
9+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
* copies of the Software, and to permit persons to whom the Software is
11+
* furnished to do so, subject to the following conditions:
12+
*
13+
* The above copyright notice and this permission notice shall be included in all
14+
* copies or substantial portions of the Software.
15+
*
16+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22+
* SOFTWARE.
23+
*/
24+
25+
package upstream
26+
27+
import (
28+
"crypto/tls"
29+
"errors"
30+
"net/http"
31+
"time"
32+
33+
"github.com/freman/caddy2-reauth/backends"
34+
"github.com/freman/caddy2-reauth/jsontypes"
35+
)
36+
37+
// Interface guard
38+
var _ backends.Driver = (*Upstream)(nil)
39+
40+
// BackendName name
41+
const BackendName = "upstream"
42+
43+
const defaultTimeout = time.Minute
44+
45+
// Upstream backend provides authentication against an upstream http server.
46+
// If the upstream request returns a http 200 status code then the user
47+
// is considered logged in.
48+
type Upstream struct {
49+
URL *jsontypes.URL `json:"url,omitempty"`
50+
Timeout jsontypes.Duration `json:"timeout,omitempty"`
51+
InsecureSkipVerify bool `json:"insecure_skip_verify,omitempty"`
52+
FollowRedirects bool `json:"follow_redirects,omitempty"`
53+
PassCookies bool `json:"pass_cookies,omitempty"`
54+
Match *jsontypes.Regexp `json:"match,omitempty"`
55+
56+
Forward struct {
57+
URL bool `json:"url,omitempty"`
58+
Method bool `json:"method,omitempty"`
59+
IP bool `json:"ip,omitempty"`
60+
Headers []string `json:"headers,omitempty"`
61+
} `json:"forward"`
62+
}
63+
64+
func noRedirectsPolicy(req *http.Request, via []*http.Request) error {
65+
return errors.New("follow redirects disabled")
66+
}
67+
68+
// NewDriver returns a new instance of Upstream with some defaults
69+
func NewDriver() *Upstream {
70+
return &Upstream{
71+
Timeout: jsontypes.Duration{Duration: defaultTimeout},
72+
}
73+
}
74+
75+
// Validate verifies that this module is functional with the given configuration
76+
func (h Upstream) Validate() error {
77+
if h.URL == nil {
78+
return errors.New("url to auth against is a required parameter")
79+
}
80+
81+
if h.Timeout.Duration <= 0 {
82+
return errors.New("timeout must be greater than 0")
83+
}
84+
85+
return nil
86+
}
87+
88+
// Authenticate fulfils the backend interface
89+
func (h Upstream) Authenticate(r *http.Request) (string, error) {
90+
un, pw, k := r.BasicAuth()
91+
if !(k || h.PassCookies) {
92+
return "", nil
93+
}
94+
95+
c := &http.Client{
96+
Timeout: h.Timeout.Duration,
97+
}
98+
99+
if !h.FollowRedirects {
100+
c.CheckRedirect = noRedirectsPolicy
101+
}
102+
103+
if h.URL.Scheme == "https" && h.InsecureSkipVerify {
104+
c.Transport = &http.Transport{
105+
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
106+
}
107+
}
108+
109+
req, err := http.NewRequest("GET", h.URL.String(), nil)
110+
if err != nil {
111+
return "", err
112+
}
113+
114+
if k {
115+
req.SetBasicAuth(un, pw)
116+
}
117+
118+
h.copyRequest(r, req)
119+
120+
resp, err := c.Do(req)
121+
if err != nil {
122+
return "", err
123+
}
124+
125+
resp.Body.Close()
126+
127+
if resp.StatusCode != 200 {
128+
return "", nil
129+
}
130+
131+
if h.Match != nil && h.Match.MatchString(resp.Request.URL.String()) {
132+
return "", nil
133+
}
134+
135+
return un, nil
136+
}
137+
138+
func (h Upstream) copyRequest(org *http.Request, req *http.Request) {
139+
if h.PassCookies {
140+
for _, c := range org.Cookies() {
141+
req.AddCookie(c)
142+
}
143+
}
144+
145+
if h.Forward.URL {
146+
req.Header.Add("X-Auth-URL", org.RequestURI)
147+
}
148+
149+
if h.Forward.Method {
150+
req.Header.Add("X-Auth-Method", org.Method)
151+
}
152+
153+
if h.Forward.IP {
154+
req.Header.Add("X-Auth-IP", org.RemoteAddr)
155+
}
156+
157+
for _, header := range h.Forward.Headers {
158+
if tmp := org.Header.Get(header); tmp != "" {
159+
req.Header.Add("X-Auth-Header-"+header, tmp)
160+
}
161+
}
162+
}

‎failure.go

+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package reauth
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"net/http"
7+
8+
"github.com/freman/caddy2-reauth/failures"
9+
"github.com/freman/caddy2-reauth/failures/basic"
10+
"github.com/freman/caddy2-reauth/failures/redirect"
11+
"github.com/freman/caddy2-reauth/failures/status"
12+
)
13+
14+
// Failure is a failure mode
15+
type Failure struct {
16+
Mode string `json:"mode,omitempty"`
17+
driver failures.Driver
18+
}
19+
20+
// Handle handles the failure mode.
21+
func (f *Failure) Handle(w http.ResponseWriter, r *http.Request) error {
22+
return f.driver.Handle(w, r)
23+
}
24+
25+
// Validate checks whether an failure mode is functional.
26+
func (f *Failure) Validate() error {
27+
if f.driver == nil {
28+
f.Mode = status.FailureMode
29+
f.driver = status.NewDriver()
30+
}
31+
return f.driver.Validate()
32+
}
33+
34+
// MarshalJSON packs configuration info JSON byte array
35+
func (f Failure) MarshalJSON() ([]byte, error) {
36+
return json.Marshal(f.driver)
37+
}
38+
39+
// UnmarshalJSON unpacks configuration into appropriate structures.
40+
func (f *Failure) UnmarshalJSON(data []byte) error {
41+
if len(data) < 10 {
42+
return fmt.Errorf("invalid configuration: %s", data)
43+
}
44+
45+
type undecorated Failure
46+
var failure undecorated
47+
48+
if err := json.Unmarshal(data, &failure); err != nil {
49+
return fmt.Errorf("invalid reauth configuration, error: %s, config: %s", err, data)
50+
}
51+
52+
var driver failures.Driver
53+
switch failure.Mode {
54+
case basic.FailureMode:
55+
driver = basic.NewDriver()
56+
case redirect.FailureMode:
57+
driver = redirect.NewDriver()
58+
case status.FailureMode:
59+
driver = status.NewDriver()
60+
default:
61+
return fmt.Errorf("invalid reauth configuration, error: unknown failure mode %q, config: %s", failure.Mode, data)
62+
}
63+
64+
if err := json.Unmarshal(data, driver); err != nil {
65+
return fmt.Errorf("invalid reauth:%s configuration, error: %s, config:%s", failure.Mode, err, data)
66+
}
67+
68+
if err := driver.Validate(); err != nil {
69+
return fmt.Errorf("invalid reauth:%s configuration, error: %s, config: %s", failure.Mode, err, data)
70+
}
71+
72+
f.Mode = failure.Mode
73+
f.driver = driver
74+
return nil
75+
}

‎failures/basic/failure.go

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package basic
2+
3+
import (
4+
"net/http"
5+
6+
"github.com/freman/caddy2-reauth/failures"
7+
)
8+
9+
type Basic struct {
10+
Realm string `json:"realm,omitempty"`
11+
}
12+
13+
// FailureMode name
14+
const FailureMode = "httpbasic"
15+
16+
// Interface guard
17+
var _ failures.Driver = (*Basic)(nil)
18+
19+
// NewDriver returns an instance of Basic
20+
func NewDriver() *Basic {
21+
return &Basic{}
22+
}
23+
24+
// Validate verifies that this module is functional with the given configuration
25+
func (h Basic) Validate() error {
26+
return nil
27+
}
28+
29+
// Handle the failure
30+
func (h Basic) Handle(w http.ResponseWriter, r *http.Request) error {
31+
realm := r.Host
32+
33+
if h.Realm != "" {
34+
realm = h.Realm
35+
}
36+
37+
w.Header().Add("WWW-Authenticate", `Basic realm="`+realm+`"`)
38+
w.WriteHeader(http.StatusUnauthorized)
39+
40+
return nil
41+
}

‎failures/driver.go

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package failures
2+
3+
import "net/http"
4+
5+
// Driver is an interface to an failure provider.
6+
type Driver interface {
7+
Handle(w http.ResponseWriter, r *http.Request) error
8+
Validate() error
9+
}

‎failures/redirect/failure.go

+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package redirect
2+
3+
import (
4+
"errors"
5+
"net/http"
6+
"net/url"
7+
"strings"
8+
9+
"github.com/freman/caddy2-reauth/failures"
10+
"github.com/freman/caddy2-reauth/jsontypes"
11+
)
12+
13+
// FailureMode name
14+
const FailureMode = "redirect"
15+
16+
const defaultRedirectCode = 303
17+
18+
// Interface guard
19+
var _ failures.Driver = (*Redirect)(nil)
20+
21+
type Redirect struct {
22+
URL *jsontypes.URL `json:"url,omitempty"`
23+
Code int `json:"code,omitempty"`
24+
}
25+
26+
// NewDriver returns a new instance of Redirect
27+
func NewDriver() *Redirect {
28+
return &Redirect{
29+
Code: defaultRedirectCode,
30+
}
31+
}
32+
33+
// Validate verifies that this module is functional with the given configuration
34+
func (h Redirect) Validate() error {
35+
if h.URL == nil {
36+
return errors.New("url to redirect to is a required parameter")
37+
}
38+
39+
return nil
40+
}
41+
42+
// Handle the error
43+
func (h Redirect) Handle(w http.ResponseWriter, r *http.Request) error {
44+
uri := r.URL
45+
uri.Host = ""
46+
uri.Scheme = ""
47+
48+
// Handle redirection back to hosts that aren't the auth server.
49+
if h.URL.Host != "" && h.URL.Host != r.Host {
50+
uri.Host = r.Host
51+
uri.Scheme = "http"
52+
if r.TLS != nil || strings.EqualFold(r.Header.Get("X-Forwarded-Proto"), "https") {
53+
uri.Scheme = "https"
54+
}
55+
}
56+
57+
redirect := strings.Replace(h.URL.String(), "{uri}", url.QueryEscape(uri.String()), -1)
58+
w.Header().Add("Location", redirect)
59+
http.Redirect(w, r, redirect, h.Code)
60+
61+
return nil
62+
}

‎failures/status/failure.go

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package status
2+
3+
import (
4+
"net/http"
5+
6+
"github.com/freman/caddy2-reauth/failures"
7+
)
8+
9+
// FailureMode name
10+
const FailureMode = "status"
11+
12+
const defaultCode = http.StatusForbidden
13+
14+
// Interface guard
15+
var _ failures.Driver = (*Status)(nil)
16+
17+
// Status simply returns a http status code
18+
type Status struct {
19+
Code int `json:"code,omitempty"`
20+
}
21+
22+
// NewDriver returns an instance of Status with some configured defaults
23+
func NewDriver() *Status {
24+
return &Status{
25+
Code: defaultCode,
26+
}
27+
}
28+
29+
// Validate verifies that this module is functional with the given configuration
30+
func (h Status) Validate() error {
31+
return nil
32+
}
33+
34+
// Handle the failure
35+
func (h Status) Handle(w http.ResponseWriter, r *http.Request) error {
36+
w.WriteHeader(h.Code)
37+
return nil
38+
}

‎go.mod

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
module github.com/freman/caddy2-reauth
2+
3+
go 1.14
4+
5+
require (
6+
github.com/caddyserver/caddy/v2 v2.0.0
7+
github.com/go-ldap/ldap/v3 v3.1.10
8+
go.uber.org/zap v1.15.0
9+
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37
10+
)

‎go.sum

+1,036
Large diffs are not rendered by default.

‎jsontypes/duration.go

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package jsontypes
2+
3+
import (
4+
"encoding/json"
5+
"time"
6+
)
7+
8+
type Duration struct {
9+
time.Duration
10+
}
11+
12+
func (d *Duration) UnmarshalJSON(data []byte) error {
13+
var s string
14+
if err := json.Unmarshal(data, &s); err != nil {
15+
return err
16+
}
17+
return d.Unmarshal(s)
18+
}
19+
20+
func (d *Duration) Unmarshal(s string) (err error) {
21+
d.Duration, err = time.ParseDuration(s)
22+
return
23+
}
24+
25+
func (d Duration) MarshalJSON() ([]byte, error) {
26+
return json.Marshal(d.Duration.String())
27+
}

‎jsontypes/regexp.go

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package jsontypes
2+
3+
import (
4+
"encoding/json"
5+
"regexp"
6+
)
7+
8+
type Regexp struct {
9+
*regexp.Regexp
10+
}
11+
12+
func (r *Regexp) UnmarshalJSON(data []byte) error {
13+
var s string
14+
err := json.Unmarshal(data, &s)
15+
if err != nil {
16+
return err
17+
}
18+
return r.Unmarshal(s)
19+
}
20+
21+
func (r *Regexp) Unmarshal(s string) (err error) {
22+
r.Regexp, err = regexp.Compile(s)
23+
return
24+
}
25+
26+
func (r *Regexp) MarshalJSON() ([]byte, error) {
27+
return json.Marshal(r.Regexp.String())
28+
}

‎jsontypes/url.go

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package jsontypes
2+
3+
import (
4+
"encoding/json"
5+
"net/url"
6+
)
7+
8+
type URL struct {
9+
*url.URL
10+
}
11+
12+
func (u URL) MarshalJSON() ([]byte, error) {
13+
return json.Marshal(u.URL.String())
14+
}
15+
16+
func (u *URL) UnmarshalJSON(data []byte) error {
17+
var s string
18+
err := json.Unmarshal(data, &s)
19+
if err != nil {
20+
return err
21+
}
22+
return u.Unmarshal(s)
23+
}
24+
25+
func (u *URL) Unmarshal(s string) (err error) {
26+
u.URL, err = url.Parse(s)
27+
return
28+
}

‎reauth.go

+79
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package reauth
2+
3+
import (
4+
"fmt"
5+
"net/http"
6+
7+
"github.com/caddyserver/caddy/v2"
8+
"github.com/caddyserver/caddy/v2/modules/caddyhttp/caddyauth"
9+
"go.uber.org/zap"
10+
)
11+
12+
func init() {
13+
caddy.RegisterModule(Reauth{})
14+
}
15+
16+
// Reauth module
17+
type Reauth struct {
18+
Backends []Backend `json:"backends,omitempty"`
19+
Failure Failure `json:"failure,omitempty"`
20+
21+
logger *zap.Logger
22+
}
23+
24+
// CaddyModule returns the Caddy module information.
25+
func (Reauth) CaddyModule() caddy.ModuleInfo {
26+
return caddy.ModuleInfo{
27+
ID: "http.authentication.providers.reauth",
28+
New: func() caddy.Module { return new(Reauth) },
29+
}
30+
}
31+
32+
// Provision implements caddy.Provisioner.
33+
func (r *Reauth) Provision(ctx caddy.Context) error {
34+
r.logger = ctx.Logger(r)
35+
r.logger.Info("provisioning plugin instance")
36+
return nil
37+
}
38+
39+
// Validate implements caddy.Validator.
40+
func (r Reauth) Validate() error {
41+
for i, be := range r.Backends {
42+
if err := be.Validate(); err != nil {
43+
return fmt.Errorf("backends[%d] (%s) failed validation: %s", i, be.Type, err)
44+
}
45+
}
46+
47+
if err := r.Failure.Validate(); err != nil {
48+
return fmt.Errorf("failure mode %s failed validation: %s", r.Failure.Mode, err)
49+
}
50+
51+
return nil
52+
}
53+
54+
// Authenticate the request
55+
func (r Reauth) Authenticate(w http.ResponseWriter, req *http.Request) (caddyauth.User, bool, error) {
56+
for _, b := range r.Backends {
57+
user, err := b.Authenticate(req)
58+
if err != nil {
59+
return caddyauth.User{}, false, err
60+
}
61+
if user != "" {
62+
return caddyauth.User{
63+
ID: user,
64+
Metadata: map[string]string{
65+
"reauth_backend": b.Type,
66+
},
67+
}, true, nil
68+
}
69+
}
70+
71+
return caddyauth.User{}, false, r.Failure.Handle(w, req)
72+
}
73+
74+
// Interface guards
75+
var (
76+
_ caddy.Provisioner = (*Reauth)(nil)
77+
_ caddy.Validator = (*Reauth)(nil)
78+
_ caddyauth.Authenticator = (*Reauth)(nil)
79+
)

0 commit comments

Comments
 (0)
Please sign in to comment.