initial commit
This commit is contained in:
commit
5b46b7a0aa
20
Dockerfile
Normal file
20
Dockerfile
Normal file
@ -0,0 +1,20 @@
|
||||
FROM golang:1.10-alpine as builder
|
||||
|
||||
# Setup
|
||||
RUN mkdir /app
|
||||
WORKDIR /app
|
||||
|
||||
# Add libraries
|
||||
RUN apk add --no-cache git && \
|
||||
go get "github.com/namsral/flag" && \
|
||||
go get "github.com/op/go-logging" && \
|
||||
apk del git
|
||||
|
||||
# Copy & build
|
||||
ADD . /app/
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix nocgo -o /traefik-forward-auth .
|
||||
|
||||
# Copy into scratch container
|
||||
FROM scratch
|
||||
COPY --from=builder /traefik-forward-auth ./
|
||||
ENTRYPOINT ["./traefik-forward-auth"]
|
21
LICENSE.md
Normal file
21
LICENSE.md
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) [2018] [Thom Seddon]
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
64
README.md
Normal file
64
README.md
Normal file
@ -0,0 +1,64 @@
|
||||
|
||||
# Traefik Forward Auth
|
||||
|
||||
A minimal forward authentication service that provides Google oauth based login and authentication for the traefik reverse proxy.
|
||||
|
||||
|
||||
## Why?
|
||||
|
||||
- Seamlessly overlays any http service with a single endpoint (see: `-url-path` in [Configuration](#configuration))
|
||||
- Supports multiple domains/subdomains
|
||||
- Allows authentication to persist across multiple domains (see [Cookie Domains](#cookie-domains))
|
||||
- Supports extended authentication beyond Google token lifetime (see: `-lifetime` in [Configuration](#configuration))
|
||||
|
||||
## Quick Start
|
||||
|
||||
See the (examples) directory for example docker compose and traefik configuration files that demonstrates the forward authentication configuration for traefik and passing required configuration values to traefik-forward-auth.
|
||||
|
||||
## Configuration
|
||||
|
||||
The following configuration is supported:
|
||||
|
||||
|
||||
|Flag |Type |Description|
|
||||
|-----------------------|------|-----------|
|
||||
|-client-id|string|*Google Client ID (required)|
|
||||
|-client-secret|string|*Google Client Secret (required)|
|
||||
|-config|string|Path to config file|
|
||||
|-cookie-domains|string|Comma separated list of cookie domains|
|
||||
|-cookie-name|string|Cookie Name (default "_forward_auth")|
|
||||
|-cookie-secret|string|*Cookie secret (required)|
|
||||
|-cookie-secure|bool|Use secure cookies (default true)|
|
||||
|-csrf-cookie-name|string|CSRF Cookie Name (default "_forward_auth_csrf")|
|
||||
|-direct|bool|Run in direct mode (use own hostname as oppose to <br>X-Forwarded-Host, used for testing/development)
|
||||
|-domain|string|Comma separated list of email domains to allow|
|
||||
|-lifetime|int|Session length in seconds (default 43200)|
|
||||
|-url-path|string|Callback URL (default "_oauth")|
|
||||
|
||||
Configuration can also be supplied as environment variables (use upper case and swap `-`'s for `_`'s e.g. `-client-id` becomes `CLIENT_ID`)
|
||||
|
||||
Configuration can also be supplied via a file, you can specify the location with `-config` flag, the format is `flag value` one per line, e.g. `client-id your-client-id`)
|
||||
|
||||
## OAuth Configuration
|
||||
|
||||
Head to https://console.developers.google.com & make sure you've switched to the correct email account.
|
||||
|
||||
Create a new project then search for and select "Credentials" in the search bar. Fill out the "OAuth Consent Screen" tab.
|
||||
|
||||
Click, "Create Credentials" > "OAuth client ID". Select "Web Application", fill in the name of your app, skip "Authorized JavaScript origins" and fill "Authorized redirect URIs" with all the domains you will allow authentication from, appended with the `url-path` (e.g. https://app.test.com/_oauth)
|
||||
|
||||
## Cookie Domains
|
||||
|
||||
You can supply a comma separated list of cookie domains, if the host of the original request is a subdomain of any given cookie domain, the authentication cookie will set with the given domain.
|
||||
|
||||
For example, if cookie domain is `test.com` and a request comes in on `app1.test.com`, the cookie will be set for the whole `test.com` domain. As such, if another request is forwarded for authentication from `app2.test.com`, the original cookie will be sent and so the request will be allowed without further authentication.
|
||||
|
||||
Beware however, if using cookie domains whilst running multiple instances of traefik/traefik-forward-auth for the same domain, the cookies will clash. You can fix this by using the same `cookie-secret` in both instances, or using a different `cookie-name` on each.
|
||||
|
||||
## Copyright
|
||||
|
||||
2018 Thom Seddon
|
||||
|
||||
## License
|
||||
|
||||
[MIT](https://github.com/thomseddon/traefik-forward-auth/blob/master/LICENSE.md)
|
37
example/docker-compose.yml
Normal file
37
example/docker-compose.yml
Normal file
@ -0,0 +1,37 @@
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
traefik:
|
||||
image: traefik
|
||||
command: -c /traefik.toml --logLevel=DEBUG
|
||||
ports:
|
||||
- "8085:80"
|
||||
- "8086:8080"
|
||||
networks:
|
||||
- traefik
|
||||
volumes:
|
||||
- ./traefik.toml:/traefik.toml
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
|
||||
whoami1:
|
||||
image: emilevauge/whoami
|
||||
networks:
|
||||
- traefik
|
||||
labels:
|
||||
- "traefik.backend=whoami"
|
||||
- "traefik.enable=true"
|
||||
- "traefik.frontend.rule=Host:whoami.localhost.com"
|
||||
|
||||
forward-oauth:
|
||||
image: thomseddon/traefik-forward-auth
|
||||
environment:
|
||||
- CLIENT_ID=your-client-id
|
||||
- CLIENT_SECRET=your-client-secret
|
||||
- COOKIE_SECRET=something-random
|
||||
- COOKIE_SECURE=false
|
||||
- DOMAIN=yourcompany.com
|
||||
networks:
|
||||
- traefik
|
||||
|
||||
networks:
|
||||
traefik:
|
136
example/traefik.toml
Normal file
136
example/traefik.toml
Normal file
@ -0,0 +1,136 @@
|
||||
################################################################
|
||||
# Global configuration
|
||||
################################################################
|
||||
|
||||
# Enable debug mode
|
||||
#
|
||||
# Optional
|
||||
# Default: false
|
||||
#
|
||||
# debug = true
|
||||
|
||||
# Log level
|
||||
#
|
||||
# Optional
|
||||
# Default: "ERROR"
|
||||
#
|
||||
# logLevel = "DEBUG"
|
||||
|
||||
# Entrypoints to be used by frontends that do not specify any entrypoint.
|
||||
# Each frontend can specify its own entrypoints.
|
||||
#
|
||||
# Optional
|
||||
# Default: ["http"]
|
||||
#
|
||||
# defaultEntryPoints = ["http", "https"]
|
||||
|
||||
################################################################
|
||||
# Entrypoints configuration
|
||||
################################################################
|
||||
|
||||
# Entrypoints definition
|
||||
#
|
||||
# Optional
|
||||
# Default:
|
||||
[entryPoints]
|
||||
[entryPoints.http]
|
||||
address = ":80"
|
||||
|
||||
[entryPoints.http.auth.forward]
|
||||
address = "http://forward-oauth:4181"
|
||||
|
||||
################################################################
|
||||
# Traefik logs configuration
|
||||
################################################################
|
||||
|
||||
# Traefik logs
|
||||
# Enabled by default and log to stdout
|
||||
#
|
||||
# Optional
|
||||
#
|
||||
# [traefikLog]
|
||||
|
||||
# Sets the filepath for the traefik log. If not specified, stdout will be used.
|
||||
# Intermediate directories are created if necessary.
|
||||
#
|
||||
# Optional
|
||||
# Default: os.Stdout
|
||||
#
|
||||
# filePath = "log/traefik.log"
|
||||
|
||||
# Format is either "json" or "common".
|
||||
#
|
||||
# Optional
|
||||
# Default: "common"
|
||||
#
|
||||
# format = "common"
|
||||
|
||||
################################################################
|
||||
# Access logs configuration
|
||||
################################################################
|
||||
|
||||
# Enable access logs
|
||||
# By default it will write to stdout and produce logs in the textual
|
||||
# Common Log Format (CLF), extended with additional fields.
|
||||
#
|
||||
# Optional
|
||||
#
|
||||
# [accessLog]
|
||||
|
||||
# Sets the file path for the access log. If not specified, stdout will be used.
|
||||
# Intermediate directories are created if necessary.
|
||||
#
|
||||
# Optional
|
||||
# Default: os.Stdout
|
||||
#
|
||||
# filePath = "/path/to/log/log.txt"
|
||||
|
||||
# Format is either "json" or "common".
|
||||
#
|
||||
# Optional
|
||||
# Default: "common"
|
||||
#
|
||||
# format = "common"
|
||||
|
||||
################################################################
|
||||
# API and dashboard configuration
|
||||
################################################################
|
||||
|
||||
# Enable API and dashboard
|
||||
[api]
|
||||
|
||||
# Name of the related entry point
|
||||
#
|
||||
# Optional
|
||||
# Default: "traefik"
|
||||
#
|
||||
# entryPoint = "traefik"
|
||||
|
||||
# Enabled Dashboard
|
||||
#
|
||||
# Optional
|
||||
# Default: true
|
||||
#
|
||||
# dashboard = false
|
||||
|
||||
################################################################
|
||||
# Ping configuration
|
||||
################################################################
|
||||
|
||||
# Enable ping
|
||||
[ping]
|
||||
|
||||
# Name of the related entry point
|
||||
#
|
||||
# Optional
|
||||
# Default: "traefik"
|
||||
#
|
||||
# entryPoint = "traefik"
|
||||
|
||||
################################################################
|
||||
# Docker configuration backend
|
||||
################################################################
|
||||
|
||||
# Enable Docker configuration backend
|
||||
[docker]
|
||||
exposedByDefault = false
|
358
forwardauth.go
Normal file
358
forwardauth.go
Normal file
@ -0,0 +1,358 @@
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
"strings"
|
||||
"strconv"
|
||||
"net/url"
|
||||
"net/http"
|
||||
"crypto/hmac"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/json"
|
||||
"encoding/base64"
|
||||
)
|
||||
|
||||
// Forward Auth
|
||||
type ForwardAuth struct {
|
||||
Path string
|
||||
PathLen int
|
||||
Lifetime time.Duration
|
||||
|
||||
ClientId string
|
||||
ClientSecret string
|
||||
Scope string
|
||||
|
||||
LoginURL *url.URL
|
||||
TokenURL *url.URL
|
||||
UserURL *url.URL
|
||||
|
||||
CookieName string
|
||||
CookieDomains []CookieDomain
|
||||
CSRFCookieName string
|
||||
CookieSecret []byte
|
||||
CookieSecure bool
|
||||
|
||||
Domain []string
|
||||
|
||||
Direct bool
|
||||
}
|
||||
|
||||
// Request Validation
|
||||
|
||||
// Cookie = hash(secret, cookie domain, email, expires)|expires|email
|
||||
func (f *ForwardAuth) ValidateCookie(r *http.Request, c *http.Cookie) (bool, string) {
|
||||
parts := strings.Split(c.Value, "|")
|
||||
|
||||
if len(parts) != 3 {
|
||||
return false, ""
|
||||
}
|
||||
|
||||
mac, err := base64.URLEncoding.DecodeString(parts[0])
|
||||
if err != nil {
|
||||
return false, ""
|
||||
}
|
||||
|
||||
expectedSignature := f.cookieSignature(r, parts[2], parts[1])
|
||||
expected, err := base64.URLEncoding.DecodeString(expectedSignature)
|
||||
if err != nil {
|
||||
return false, ""
|
||||
}
|
||||
|
||||
// Valid token?
|
||||
if !hmac.Equal(mac, expected) {
|
||||
return false, ""
|
||||
}
|
||||
|
||||
expires, err := strconv.ParseInt(parts[1], 10, 64)
|
||||
if err != nil {
|
||||
return false, ""
|
||||
}
|
||||
|
||||
// Has it expired?
|
||||
if time.Unix(expires, 0).Before(time.Now()) {
|
||||
return false, ""
|
||||
}
|
||||
|
||||
// Looks valid
|
||||
return true, parts[2]
|
||||
}
|
||||
|
||||
// Validate email
|
||||
func (f *ForwardAuth) ValidateEmail(email string) bool {
|
||||
if len(f.Domain) > 0 {
|
||||
parts := strings.Split(email, "@")
|
||||
if len(parts) < 2 {
|
||||
return false
|
||||
}
|
||||
found := false
|
||||
for _, domain := range f.Domain {
|
||||
if domain == parts[1] {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
// OAuth Methods
|
||||
|
||||
// Get login url
|
||||
func (f *ForwardAuth) GetLoginURL(r *http.Request, nonce string) string {
|
||||
state := fmt.Sprintf("%s:%s", nonce, f.ReturnUrl(r))
|
||||
|
||||
q := url.Values{}
|
||||
q.Set("client_id", fw.ClientId)
|
||||
q.Set("response_type", "code")
|
||||
q.Set("scope", fw.Scope)
|
||||
// q.Set("approval_prompt", fw.ClientId)
|
||||
q.Set("redirect_uri", f.RedirectUri(r))
|
||||
q.Set("state", state)
|
||||
|
||||
var u url.URL
|
||||
u = *fw.LoginURL
|
||||
u.RawQuery = q.Encode()
|
||||
|
||||
return u.String()
|
||||
}
|
||||
|
||||
// Exchange code for token
|
||||
|
||||
type Token struct {
|
||||
Token string `json:"access_token"`
|
||||
}
|
||||
|
||||
func (f *ForwardAuth) ExchangeCode(r *http.Request, code, redirect string) (string, error) {
|
||||
form := url.Values{}
|
||||
form.Set("client_id", fw.ClientId)
|
||||
form.Set("client_secret", fw.ClientSecret)
|
||||
form.Set("grant_type", "authorization_code")
|
||||
form.Set("redirect_uri", f.RedirectUri(r))
|
||||
form.Set("code", code)
|
||||
|
||||
|
||||
res, err := http.PostForm(fw.TokenURL.String(), form)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var token Token
|
||||
defer res.Body.Close()
|
||||
err = json.NewDecoder(res.Body).Decode(&token)
|
||||
|
||||
return token.Token, err
|
||||
}
|
||||
|
||||
// Get user with token
|
||||
|
||||
type User struct {
|
||||
Id string `json:"id"`
|
||||
Email string `json:"email"`
|
||||
Verified bool `json:"verified_email"`
|
||||
Hd string `json:"hd"`
|
||||
}
|
||||
|
||||
func (f *ForwardAuth) GetUser(token string) (User, error) {
|
||||
var user User
|
||||
|
||||
client := &http.Client{}
|
||||
req, err := http.NewRequest("GET", fw.UserURL.String(), nil)
|
||||
if err != nil {
|
||||
return user, err
|
||||
}
|
||||
|
||||
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
return user, err
|
||||
}
|
||||
|
||||
defer res.Body.Close()
|
||||
err = json.NewDecoder(res.Body).Decode(&user)
|
||||
|
||||
return user, err
|
||||
}
|
||||
|
||||
// Utility methods
|
||||
|
||||
// Get the redirect base
|
||||
func (f *ForwardAuth) RedirectBase(r *http.Request) string {
|
||||
proto := r.Header.Get("X-Forwarded-Proto")
|
||||
host := r.Header.Get("X-Forwarded-Host")
|
||||
|
||||
// Direct mode
|
||||
if f.Direct {
|
||||
proto = "http"
|
||||
host = r.Host
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s://%s", proto, host)
|
||||
}
|
||||
|
||||
// Return url
|
||||
func (f *ForwardAuth) ReturnUrl(r *http.Request) string {
|
||||
path := r.Header.Get("X-Forwarded-Uri")
|
||||
|
||||
// Testing
|
||||
if f.Direct {
|
||||
path = r.URL.String()
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s%s", f.RedirectBase(r), path)
|
||||
}
|
||||
|
||||
// Get oauth redirect uri
|
||||
func (f *ForwardAuth) RedirectUri(r *http.Request) string {
|
||||
return fmt.Sprintf("%s%s", f.RedirectBase(r), f.Path)
|
||||
}
|
||||
|
||||
// Cookie methods
|
||||
|
||||
// Create an auth cookie
|
||||
func (f *ForwardAuth) MakeCookie(r *http.Request, email string) *http.Cookie {
|
||||
expires := f.cookieExpiry()
|
||||
mac := f.cookieSignature(r, email, fmt.Sprintf("%d", expires.Unix()))
|
||||
value := fmt.Sprintf("%s|%d|%s", mac, expires.Unix(), email)
|
||||
|
||||
return &http.Cookie{
|
||||
Name: f.CookieName,
|
||||
Value: value,
|
||||
Path: "/",
|
||||
Domain: f.cookieDomain(r),
|
||||
HttpOnly: true,
|
||||
Secure: f.CookieSecure,
|
||||
Expires: expires,
|
||||
}
|
||||
}
|
||||
|
||||
// Make a CSRF cookie (used during login only)
|
||||
func (f *ForwardAuth) MakeCSRFCookie(r *http.Request, nonce string) *http.Cookie {
|
||||
return &http.Cookie{
|
||||
Name: f.CSRFCookieName,
|
||||
Value: nonce,
|
||||
Path: "/",
|
||||
Domain: f.cookieDomain(r),
|
||||
HttpOnly: true,
|
||||
Secure: f.CookieSecure,
|
||||
Expires: f.cookieExpiry(),
|
||||
}
|
||||
}
|
||||
|
||||
// Create a cookie to clear csrf cookie
|
||||
func (f *ForwardAuth) ClearCSRFCookie(r *http.Request) *http.Cookie {
|
||||
return &http.Cookie{
|
||||
Name: f.CSRFCookieName,
|
||||
Value: "",
|
||||
Path: "/",
|
||||
Domain: f.cookieDomain(r),
|
||||
HttpOnly: true,
|
||||
Secure: f.CookieSecure,
|
||||
Expires: time.Now().Local().Add(time.Hour * -1),
|
||||
}
|
||||
}
|
||||
|
||||
// Validate the csrf cookie against state
|
||||
func (f *ForwardAuth) ValidateCSRFCookie(c *http.Cookie, state string) (bool, string) {
|
||||
if len(c.Value) != 32 {
|
||||
return false, ""
|
||||
}
|
||||
|
||||
if len(state) < 34 {
|
||||
return false, ""
|
||||
}
|
||||
|
||||
// Check nonce match
|
||||
if c.Value != state[:32] {
|
||||
return false, ""
|
||||
}
|
||||
|
||||
// Valid, return redirect
|
||||
return true, state[33:]
|
||||
}
|
||||
|
||||
func (f *ForwardAuth) Nonce() (error, string) {
|
||||
// Make nonce
|
||||
nonce := make([]byte, 16)
|
||||
_, err := rand.Read(nonce)
|
||||
if err != nil {
|
||||
return err, ""
|
||||
}
|
||||
|
||||
return nil, fmt.Sprintf("%x", nonce)
|
||||
}
|
||||
|
||||
// Cookie domain
|
||||
func (f *ForwardAuth) cookieDomain(r *http.Request) string {
|
||||
host := r.Header.Get("X-Forwarded-Host")
|
||||
|
||||
// Direct mode
|
||||
if f.Direct {
|
||||
host = r.Host
|
||||
}
|
||||
|
||||
// Remove port for matching
|
||||
p := strings.Split(host, ":")
|
||||
|
||||
// Check if any of the given cookie domains matches
|
||||
for _, domain := range f.CookieDomains {
|
||||
if domain.Match(p[0]) {
|
||||
return domain.Domain
|
||||
}
|
||||
}
|
||||
|
||||
return p[0]
|
||||
}
|
||||
|
||||
// Create cookie hmac
|
||||
func (f *ForwardAuth) cookieSignature(r *http.Request, email, expires string) string {
|
||||
hash := hmac.New(sha256.New, f.CookieSecret)
|
||||
hash.Write([]byte(f.cookieDomain(r)))
|
||||
hash.Write([]byte(email))
|
||||
hash.Write([]byte(expires))
|
||||
return base64.URLEncoding.EncodeToString(hash.Sum(nil))
|
||||
}
|
||||
|
||||
// Get cookie expirary
|
||||
func (f *ForwardAuth) cookieExpiry() time.Time {
|
||||
return time.Now().Local().Add(f.Lifetime)
|
||||
}
|
||||
|
||||
// Cookie Domain
|
||||
|
||||
// Cookie Domain
|
||||
type CookieDomain struct {
|
||||
Domain string
|
||||
DomainLen int
|
||||
SubDomain string
|
||||
SubDomainLen int
|
||||
}
|
||||
|
||||
func NewCookieDomain(domain string) *CookieDomain {
|
||||
return &CookieDomain{
|
||||
Domain: domain,
|
||||
DomainLen: len(domain),
|
||||
SubDomain: fmt.Sprintf(".%s", domain),
|
||||
SubDomainLen: len(domain) + 1,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CookieDomain) Match(host string) bool {
|
||||
// Exact domain match?
|
||||
if host == c.Domain {
|
||||
return true
|
||||
}
|
||||
|
||||
// Subdomain match?
|
||||
if len(host) >= c.SubDomainLen && host[len(host) - c.SubDomainLen:] == c.SubDomain {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
216
main.go
Normal file
216
main.go
Normal file
@ -0,0 +1,216 @@
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
"strings"
|
||||
"net/url"
|
||||
"net/http"
|
||||
|
||||
"github.com/namsral/flag"
|
||||
"github.com/op/go-logging"
|
||||
)
|
||||
|
||||
// Vars
|
||||
var fw *ForwardAuth;
|
||||
var log = logging.MustGetLogger("traefik-forward-auth")
|
||||
|
||||
// Primary handler
|
||||
func handler(w http.ResponseWriter, r *http.Request) {
|
||||
// Parse uri
|
||||
uri, err := url.Parse(r.Header.Get("X-Forwarded-Uri"))
|
||||
if err != nil {
|
||||
log.Error("Error parsing url")
|
||||
http.Error(w, "Service unavailable", 503)
|
||||
return
|
||||
}
|
||||
|
||||
// Direct mode
|
||||
if fw.Direct {
|
||||
uri = r.URL
|
||||
}
|
||||
|
||||
// Handle callback
|
||||
if uri.Path == fw.Path {
|
||||
handleCallback(w, r, uri.Query())
|
||||
return
|
||||
}
|
||||
|
||||
c, err := r.Cookie(fw.CookieName)
|
||||
if err != nil {
|
||||
// Error indicates no cookie, generate nonce
|
||||
err, nonce := fw.Nonce()
|
||||
if err != nil {
|
||||
log.Error("Error generating nonce")
|
||||
http.Error(w, "Service unavailable", 503)
|
||||
return
|
||||
}
|
||||
|
||||
// Set the CSRF cookie
|
||||
http.SetCookie(w, fw.MakeCSRFCookie(r, nonce))
|
||||
log.Debug("Set CSRF cookie and redirecting to google login")
|
||||
|
||||
// Forward them on
|
||||
http.Redirect(w, r, fw.GetLoginURL(r, nonce), http.StatusTemporaryRedirect)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Validate cookie
|
||||
valid, email := fw.ValidateCookie(r, c)
|
||||
if !valid {
|
||||
http.Error(w, "Not authorized", 401)
|
||||
return
|
||||
}
|
||||
|
||||
// Validate user
|
||||
valid = fw.ValidateEmail(email)
|
||||
if !valid {
|
||||
http.Error(w, "Not authorized", 401)
|
||||
return
|
||||
}
|
||||
|
||||
// Valid request
|
||||
w.WriteHeader(200)
|
||||
}
|
||||
|
||||
|
||||
// Authenticate user after they have come back from google
|
||||
func handleCallback(w http.ResponseWriter, r *http.Request, qs url.Values) {
|
||||
// Check for CSRF cookie
|
||||
csrfCookie, err := r.Cookie(fw.CSRFCookieName)
|
||||
if err != nil {
|
||||
log.Debug("Missing csrf cookie")
|
||||
http.Error(w, "Not authorized", 401)
|
||||
return
|
||||
}
|
||||
|
||||
// Validate state
|
||||
state := qs.Get("state")
|
||||
valid, redirect := fw.ValidateCSRFCookie(csrfCookie, state)
|
||||
if !valid && false {
|
||||
log.Debugf("Invalid oauth state, expected '%s', got '%s'\n", csrfCookie.Value, state)
|
||||
http.Error(w, "Not authorized", 401)
|
||||
return
|
||||
}
|
||||
|
||||
// Clear CSRF cookie
|
||||
http.SetCookie(w, fw.ClearCSRFCookie(r))
|
||||
|
||||
// Exchange code for token
|
||||
token, err := fw.ExchangeCode(r, qs.Get("code"), fw.RedirectUri(r))
|
||||
if err != nil {
|
||||
log.Debugf("Code exchange failed with: %s\n", err)
|
||||
http.Error(w, "Service unavailable", 503)
|
||||
return
|
||||
}
|
||||
|
||||
// Get user
|
||||
user, err := fw.GetUser(token)
|
||||
if err != nil {
|
||||
log.Debugf("Error getting user: %s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Generate cookie
|
||||
http.SetCookie(w, fw.MakeCookie(r, user.Email))
|
||||
log.Debugf("Generated auth cookie for %s\n", user.Email)
|
||||
|
||||
// Redirect
|
||||
http.Redirect(w, r, redirect, http.StatusTemporaryRedirect)
|
||||
}
|
||||
|
||||
|
||||
// Main
|
||||
func main() {
|
||||
// Parse options
|
||||
flag.String(flag.DefaultConfigFlagname, "", "Path to config file")
|
||||
path := flag.String("url-path", "_oauth", "Callback URL")
|
||||
lifetime := flag.Int("lifetime", 43200, "Session length in seconds")
|
||||
clientId := flag.String("client-id", "", "*Google Client ID (required)")
|
||||
clientSecret := flag.String("client-secret", "", "*Google Client Secret (required)")
|
||||
cookieName := flag.String("cookie-name", "_forward_auth", "Cookie Name")
|
||||
cSRFCookieName := flag.String("csrf-cookie-name", "_forward_auth_csrf", "CSRF Cookie Name")
|
||||
cookieDomainList := flag.String("cookie-domains", "", "Comma separated list of cookie domains") //todo
|
||||
cookieSecret := flag.String("cookie-secret", "", "*Cookie secret (required)")
|
||||
cookieSecure := flag.Bool("cookie-secure", true, "Use secure cookies")
|
||||
domainList := flag.String("domain", "", "Comma separated list of email domains to allow")
|
||||
direct := flag.Bool("direct", false, "Run in direct mode (use own hostname as oppose to X-Forwarded-Host, used for testing/development)")
|
||||
|
||||
flag.Parse()
|
||||
|
||||
// Check for show stopper errors
|
||||
err := false
|
||||
if *clientId == "" {
|
||||
err = true
|
||||
log.Critical("client-id must be set")
|
||||
}
|
||||
if *clientSecret == "" {
|
||||
err = true
|
||||
log.Critical("client-secret must be set")
|
||||
}
|
||||
if *cookieSecret == "" {
|
||||
err = true
|
||||
log.Critical("cookie-secret must be set")
|
||||
}
|
||||
if err {
|
||||
return
|
||||
}
|
||||
|
||||
// Parse lists
|
||||
var cookieDomains []CookieDomain
|
||||
if *cookieDomainList != "" {
|
||||
for _, d := range strings.Split(*cookieDomainList, ",") {
|
||||
cookieDomain := NewCookieDomain(d)
|
||||
cookieDomains = append(cookieDomains, *cookieDomain)
|
||||
}
|
||||
}
|
||||
|
||||
var domain []string
|
||||
if *domainList != "" {
|
||||
domain = strings.Split(*domainList, ",")
|
||||
}
|
||||
|
||||
// Setup
|
||||
fw = &ForwardAuth{
|
||||
Path: fmt.Sprintf("/%s", *path),
|
||||
PathLen: len(*path) + 1,
|
||||
Lifetime: time.Second * time.Duration(*lifetime),
|
||||
|
||||
ClientId: *clientId,
|
||||
ClientSecret: *clientSecret,
|
||||
Scope: "https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email",
|
||||
LoginURL: &url.URL{
|
||||
Scheme: "https",
|
||||
Host: "accounts.google.com",
|
||||
Path: "/o/oauth2/auth",
|
||||
},
|
||||
TokenURL: &url.URL{
|
||||
Scheme: "https",
|
||||
Host: "www.googleapis.com",
|
||||
Path: "/oauth2/v3/token",
|
||||
},
|
||||
UserURL: &url.URL{
|
||||
Scheme: "https",
|
||||
Host: "www.googleapis.com",
|
||||
Path: "/oauth2/v2/userinfo",
|
||||
},
|
||||
|
||||
CookieName: *cookieName,
|
||||
CSRFCookieName: *cSRFCookieName,
|
||||
CookieDomains: cookieDomains,
|
||||
CookieSecret: []byte(*cookieSecret),
|
||||
CookieSecure: *cookieSecure,
|
||||
|
||||
Domain: domain,
|
||||
|
||||
Direct: *direct,
|
||||
}
|
||||
|
||||
// Attach handler
|
||||
http.HandleFunc("/", handler)
|
||||
|
||||
log.Notice("Litening on :4181")
|
||||
log.Notice(http.ListenAndServe(":4181", nil))
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user