2019-04-12 16:12:13 +01:00
|
|
|
package tfa
|
2018-06-26 12:28:47 +01:00
|
|
|
|
|
|
|
import (
|
2019-01-22 10:50:55 +00:00
|
|
|
"crypto/hmac"
|
|
|
|
"crypto/rand"
|
|
|
|
"crypto/sha256"
|
|
|
|
"encoding/base64"
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"net/http"
|
|
|
|
"strconv"
|
|
|
|
"strings"
|
|
|
|
"time"
|
2018-06-26 12:28:47 +01:00
|
|
|
|
2019-04-12 16:12:13 +01:00
|
|
|
"github.com/thomseddon/traefik-forward-auth/internal/provider"
|
2019-01-30 16:52:47 +00:00
|
|
|
)
|
2018-06-26 12:28:47 +01:00
|
|
|
|
|
|
|
// Request Validation
|
|
|
|
|
|
|
|
// Cookie = hash(secret, cookie domain, email, expires)|expires|email
|
2019-04-12 16:12:13 +01:00
|
|
|
func ValidateCookie(r *http.Request, c *http.Cookie) (bool, string, error) {
|
2019-01-22 10:50:55 +00:00
|
|
|
parts := strings.Split(c.Value, "|")
|
|
|
|
|
|
|
|
if len(parts) != 3 {
|
|
|
|
return false, "", errors.New("Invalid cookie format")
|
|
|
|
}
|
|
|
|
|
|
|
|
mac, err := base64.URLEncoding.DecodeString(parts[0])
|
|
|
|
if err != nil {
|
|
|
|
return false, "", errors.New("Unable to decode cookie mac")
|
|
|
|
}
|
|
|
|
|
2019-04-12 16:12:13 +01:00
|
|
|
expectedSignature := cookieSignature(r, parts[2], parts[1])
|
2019-01-22 10:50:55 +00:00
|
|
|
expected, err := base64.URLEncoding.DecodeString(expectedSignature)
|
|
|
|
if err != nil {
|
|
|
|
return false, "", errors.New("Unable to generate mac")
|
|
|
|
}
|
|
|
|
|
|
|
|
// Valid token?
|
|
|
|
if !hmac.Equal(mac, expected) {
|
|
|
|
return false, "", errors.New("Invalid cookie mac")
|
|
|
|
}
|
|
|
|
|
|
|
|
expires, err := strconv.ParseInt(parts[1], 10, 64)
|
|
|
|
if err != nil {
|
|
|
|
return false, "", errors.New("Unable to parse cookie expiry")
|
|
|
|
}
|
|
|
|
|
|
|
|
// Has it expired?
|
|
|
|
if time.Unix(expires, 0).Before(time.Now()) {
|
|
|
|
return false, "", errors.New("Cookie has expired")
|
|
|
|
}
|
|
|
|
|
|
|
|
// Looks valid
|
|
|
|
return true, parts[2], nil
|
2018-06-26 12:28:47 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Validate email
|
2019-04-12 16:12:13 +01:00
|
|
|
func ValidateEmail(email string) bool {
|
2019-01-22 10:50:55 +00:00
|
|
|
found := false
|
2019-01-30 16:52:47 +00:00
|
|
|
if len(config.Whitelist) > 0 {
|
|
|
|
for _, whitelist := range config.Whitelist {
|
2019-01-22 10:50:55 +00:00
|
|
|
if email == whitelist {
|
|
|
|
found = true
|
|
|
|
}
|
|
|
|
}
|
2019-04-12 16:12:13 +01:00
|
|
|
} else if len(config.Domains) > 0 {
|
2019-01-22 10:50:55 +00:00
|
|
|
parts := strings.Split(email, "@")
|
|
|
|
if len(parts) < 2 {
|
|
|
|
return false
|
|
|
|
}
|
2019-04-12 16:12:13 +01:00
|
|
|
for _, domain := range config.Domains {
|
2019-01-22 10:50:55 +00:00
|
|
|
if domain == parts[1] {
|
|
|
|
found = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
return found
|
2018-06-26 12:28:47 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// OAuth Methods
|
|
|
|
|
|
|
|
// Get login url
|
2019-04-12 16:12:13 +01:00
|
|
|
func GetLoginURL(r *http.Request, nonce string) string {
|
|
|
|
state := fmt.Sprintf("%s:%s", nonce, returnUrl(r))
|
2019-01-22 10:50:55 +00:00
|
|
|
|
2019-01-30 16:52:47 +00:00
|
|
|
// TODO: Support multiple providers
|
2019-04-12 16:12:13 +01:00
|
|
|
return config.Providers.Google.GetLoginURL(redirectUri(r), state)
|
2018-06-26 12:28:47 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Exchange code for token
|
|
|
|
|
2019-04-12 16:12:13 +01:00
|
|
|
func ExchangeCode(r *http.Request) (string, error) {
|
2019-01-30 16:52:47 +00:00
|
|
|
code := r.URL.Query().Get("code")
|
2019-01-22 10:50:55 +00:00
|
|
|
|
2019-01-30 16:52:47 +00:00
|
|
|
// TODO: Support multiple providers
|
2019-04-12 16:12:13 +01:00
|
|
|
return config.Providers.Google.ExchangeCode(redirectUri(r), code)
|
2018-06-26 12:28:47 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Get user with token
|
|
|
|
|
2019-04-12 16:12:13 +01:00
|
|
|
func GetUser(token string) (provider.User, error) {
|
2019-01-30 16:52:47 +00:00
|
|
|
// TODO: Support multiple providers
|
|
|
|
return config.Providers.Google.GetUser(token)
|
2018-06-26 12:28:47 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Utility methods
|
|
|
|
|
|
|
|
// Get the redirect base
|
2019-04-12 16:12:13 +01:00
|
|
|
func redirectBase(r *http.Request) string {
|
2019-01-22 10:50:55 +00:00
|
|
|
proto := r.Header.Get("X-Forwarded-Proto")
|
|
|
|
host := r.Header.Get("X-Forwarded-Host")
|
2018-06-26 12:28:47 +01:00
|
|
|
|
2019-01-22 10:50:55 +00:00
|
|
|
return fmt.Sprintf("%s://%s", proto, host)
|
2018-06-26 12:28:47 +01:00
|
|
|
}
|
|
|
|
|
2019-01-30 16:52:47 +00:00
|
|
|
// // Return url
|
2019-04-12 16:12:13 +01:00
|
|
|
func returnUrl(r *http.Request) string {
|
2019-01-22 10:50:55 +00:00
|
|
|
path := r.Header.Get("X-Forwarded-Uri")
|
2018-06-26 12:28:47 +01:00
|
|
|
|
2019-04-12 16:12:13 +01:00
|
|
|
return fmt.Sprintf("%s%s", redirectBase(r), path)
|
2018-06-26 12:28:47 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Get oauth redirect uri
|
2019-04-12 16:12:13 +01:00
|
|
|
func redirectUri(r *http.Request) string {
|
|
|
|
if use, _ := useAuthDomain(r); use {
|
2019-01-22 10:50:55 +00:00
|
|
|
proto := r.Header.Get("X-Forwarded-Proto")
|
2019-01-30 16:52:47 +00:00
|
|
|
return fmt.Sprintf("%s://%s%s", proto, config.AuthHost, config.Path)
|
2019-01-22 10:50:55 +00:00
|
|
|
}
|
2018-10-29 17:39:36 +00:00
|
|
|
|
2019-04-12 16:12:13 +01:00
|
|
|
return fmt.Sprintf("%s%s", redirectBase(r), config.Path)
|
2018-06-26 12:28:47 +01:00
|
|
|
}
|
|
|
|
|
2018-10-29 17:39:36 +00:00
|
|
|
// Should we use auth host + what it is
|
2019-04-12 16:12:13 +01:00
|
|
|
func useAuthDomain(r *http.Request) (bool, string) {
|
2019-01-30 16:52:47 +00:00
|
|
|
if config.AuthHost == "" {
|
2019-01-22 10:50:55 +00:00
|
|
|
return false, ""
|
|
|
|
}
|
2018-10-29 17:39:36 +00:00
|
|
|
|
2019-01-22 10:50:55 +00:00
|
|
|
// Does the request match a given cookie domain?
|
2019-04-12 16:12:13 +01:00
|
|
|
reqMatch, reqHost := matchCookieDomains(r.Header.Get("X-Forwarded-Host"))
|
2018-10-29 17:39:36 +00:00
|
|
|
|
2019-01-22 10:50:55 +00:00
|
|
|
// Do any of the auth hosts match a cookie domain?
|
2019-04-12 16:12:13 +01:00
|
|
|
authMatch, authHost := matchCookieDomains(config.AuthHost)
|
2018-10-29 17:39:36 +00:00
|
|
|
|
2019-01-22 10:50:55 +00:00
|
|
|
// We need both to match the same domain
|
|
|
|
return reqMatch && authMatch && reqHost == authHost, reqHost
|
2018-10-29 17:39:36 +00:00
|
|
|
}
|
|
|
|
|
2018-06-26 12:28:47 +01:00
|
|
|
// Cookie methods
|
|
|
|
|
|
|
|
// Create an auth cookie
|
2019-04-12 16:12:13 +01:00
|
|
|
func MakeCookie(r *http.Request, email string) *http.Cookie {
|
|
|
|
expires := cookieExpiry()
|
|
|
|
mac := cookieSignature(r, email, fmt.Sprintf("%d", expires.Unix()))
|
2019-01-22 10:50:55 +00:00
|
|
|
value := fmt.Sprintf("%s|%d|%s", mac, expires.Unix(), email)
|
|
|
|
|
|
|
|
return &http.Cookie{
|
2019-01-30 16:52:47 +00:00
|
|
|
Name: config.CookieName,
|
2019-01-22 10:50:55 +00:00
|
|
|
Value: value,
|
|
|
|
Path: "/",
|
2019-04-12 16:12:13 +01:00
|
|
|
Domain: cookieDomain(r),
|
2019-01-22 10:50:55 +00:00
|
|
|
HttpOnly: true,
|
2019-04-18 15:07:39 +01:00
|
|
|
Secure: !config.InsecureCookie,
|
2019-01-22 10:50:55 +00:00
|
|
|
Expires: expires,
|
|
|
|
}
|
2018-06-26 12:28:47 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Make a CSRF cookie (used during login only)
|
2019-04-12 16:12:13 +01:00
|
|
|
func MakeCSRFCookie(r *http.Request, nonce string) *http.Cookie {
|
2019-01-22 10:50:55 +00:00
|
|
|
return &http.Cookie{
|
2019-01-30 16:52:47 +00:00
|
|
|
Name: config.CSRFCookieName,
|
2019-01-22 10:50:55 +00:00
|
|
|
Value: nonce,
|
|
|
|
Path: "/",
|
2019-04-12 16:12:13 +01:00
|
|
|
Domain: csrfCookieDomain(r),
|
2019-01-22 10:50:55 +00:00
|
|
|
HttpOnly: true,
|
2019-04-18 15:07:39 +01:00
|
|
|
Secure: !config.InsecureCookie,
|
2019-04-12 16:12:13 +01:00
|
|
|
Expires: cookieExpiry(),
|
2019-01-22 10:50:55 +00:00
|
|
|
}
|
2018-06-26 12:28:47 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Create a cookie to clear csrf cookie
|
2019-04-12 16:12:13 +01:00
|
|
|
func ClearCSRFCookie(r *http.Request) *http.Cookie {
|
2019-01-22 10:50:55 +00:00
|
|
|
return &http.Cookie{
|
2019-01-30 16:52:47 +00:00
|
|
|
Name: config.CSRFCookieName,
|
2019-01-22 10:50:55 +00:00
|
|
|
Value: "",
|
|
|
|
Path: "/",
|
2019-04-12 16:12:13 +01:00
|
|
|
Domain: csrfCookieDomain(r),
|
2019-01-22 10:50:55 +00:00
|
|
|
HttpOnly: true,
|
2019-04-18 15:07:39 +01:00
|
|
|
Secure: !config.InsecureCookie,
|
2019-01-22 10:50:55 +00:00
|
|
|
Expires: time.Now().Local().Add(time.Hour * -1),
|
|
|
|
}
|
2018-06-26 12:28:47 +01:00
|
|
|
}
|
|
|
|
|
2019-04-12 16:12:13 +01:00
|
|
|
// Validate the csrf cookie against state
|
|
|
|
func ValidateCSRFCookie(r *http.Request, c *http.Cookie) (bool, string, error) {
|
2019-01-30 16:52:47 +00:00
|
|
|
state := r.URL.Query().Get("state")
|
|
|
|
|
2019-01-22 10:50:55 +00:00
|
|
|
if len(c.Value) != 32 {
|
|
|
|
return false, "", errors.New("Invalid CSRF cookie value")
|
|
|
|
}
|
2018-06-26 12:28:47 +01:00
|
|
|
|
2019-01-22 10:50:55 +00:00
|
|
|
if len(state) < 34 {
|
|
|
|
return false, "", errors.New("Invalid CSRF state value")
|
|
|
|
}
|
2018-06-26 12:28:47 +01:00
|
|
|
|
2019-01-22 10:50:55 +00:00
|
|
|
// Check nonce match
|
|
|
|
if c.Value != state[:32] {
|
|
|
|
return false, "", errors.New("CSRF cookie does not match state")
|
|
|
|
}
|
2018-06-26 12:28:47 +01:00
|
|
|
|
2019-01-22 10:50:55 +00:00
|
|
|
// Valid, return redirect
|
|
|
|
return true, state[33:], nil
|
2018-06-26 12:28:47 +01:00
|
|
|
}
|
|
|
|
|
2019-04-12 16:12:13 +01:00
|
|
|
func Nonce() (error, string) {
|
2019-01-22 10:50:55 +00:00
|
|
|
// Make nonce
|
|
|
|
nonce := make([]byte, 16)
|
|
|
|
_, err := rand.Read(nonce)
|
|
|
|
if err != nil {
|
|
|
|
return err, ""
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil, fmt.Sprintf("%x", nonce)
|
2018-06-26 12:28:47 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Cookie domain
|
2019-04-12 16:12:13 +01:00
|
|
|
func cookieDomain(r *http.Request) string {
|
2019-01-22 10:50:55 +00:00
|
|
|
host := r.Header.Get("X-Forwarded-Host")
|
2018-06-26 12:28:47 +01:00
|
|
|
|
2019-01-22 10:50:55 +00:00
|
|
|
// Check if any of the given cookie domains matches
|
2019-04-12 16:12:13 +01:00
|
|
|
_, domain := matchCookieDomains(host)
|
2019-01-22 10:50:55 +00:00
|
|
|
return domain
|
2018-10-29 17:39:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Cookie domain
|
2019-04-12 16:12:13 +01:00
|
|
|
func csrfCookieDomain(r *http.Request) string {
|
2019-01-22 10:50:55 +00:00
|
|
|
var host string
|
2019-04-12 16:12:13 +01:00
|
|
|
if use, domain := useAuthDomain(r); use {
|
2019-01-22 10:50:55 +00:00
|
|
|
host = domain
|
|
|
|
} else {
|
|
|
|
host = r.Header.Get("X-Forwarded-Host")
|
|
|
|
}
|
|
|
|
|
|
|
|
// Remove port
|
|
|
|
p := strings.Split(host, ":")
|
|
|
|
return p[0]
|
2018-10-29 17:39:36 +00:00
|
|
|
}
|
2018-06-26 12:28:47 +01:00
|
|
|
|
2018-10-29 17:39:36 +00:00
|
|
|
// Return matching cookie domain if exists
|
2019-04-12 16:12:13 +01:00
|
|
|
func matchCookieDomains(domain string) (bool, string) {
|
2019-01-22 10:50:55 +00:00
|
|
|
// Remove port
|
|
|
|
p := strings.Split(domain, ":")
|
2018-10-29 17:39:36 +00:00
|
|
|
|
2019-01-30 16:52:47 +00:00
|
|
|
for _, d := range config.CookieDomains {
|
2019-01-22 10:50:55 +00:00
|
|
|
if d.Match(p[0]) {
|
|
|
|
return true, d.Domain
|
|
|
|
}
|
|
|
|
}
|
2018-06-26 12:28:47 +01:00
|
|
|
|
2019-01-22 10:50:55 +00:00
|
|
|
return false, p[0]
|
2018-06-26 12:28:47 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Create cookie hmac
|
2019-04-12 16:12:13 +01:00
|
|
|
func cookieSignature(r *http.Request, email, expires string) string {
|
|
|
|
hash := hmac.New(sha256.New, config.Secret)
|
|
|
|
hash.Write([]byte(cookieDomain(r)))
|
2019-01-22 10:50:55 +00:00
|
|
|
hash.Write([]byte(email))
|
|
|
|
hash.Write([]byte(expires))
|
|
|
|
return base64.URLEncoding.EncodeToString(hash.Sum(nil))
|
2018-06-26 12:28:47 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Get cookie expirary
|
2019-04-12 16:12:13 +01:00
|
|
|
func cookieExpiry() time.Time {
|
2019-01-30 16:52:47 +00:00
|
|
|
return time.Now().Local().Add(config.Lifetime)
|
2018-06-26 12:28:47 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Cookie Domain
|
|
|
|
|
|
|
|
// Cookie Domain
|
|
|
|
type CookieDomain struct {
|
2019-04-12 16:12:13 +01:00
|
|
|
Domain string `description:"TEST1"`
|
|
|
|
DomainLen int `description:"TEST2"`
|
|
|
|
SubDomain string `description:"TEST3"`
|
|
|
|
SubDomainLen int `description:"TEST4"`
|
2018-06-26 12:28:47 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
func NewCookieDomain(domain string) *CookieDomain {
|
2019-01-22 10:50:55 +00:00
|
|
|
return &CookieDomain{
|
|
|
|
Domain: domain,
|
|
|
|
DomainLen: len(domain),
|
|
|
|
SubDomain: fmt.Sprintf(".%s", domain),
|
|
|
|
SubDomainLen: len(domain) + 1,
|
|
|
|
}
|
2018-06-26 12:28:47 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
func (c *CookieDomain) Match(host string) bool {
|
2019-01-22 10:50:55 +00:00
|
|
|
// Exact domain match?
|
|
|
|
if host == c.Domain {
|
|
|
|
return true
|
|
|
|
}
|
2018-06-26 12:28:47 +01:00
|
|
|
|
2019-01-22 10:50:55 +00:00
|
|
|
// Subdomain match?
|
|
|
|
if len(host) >= c.SubDomainLen && host[len(host)-c.SubDomainLen:] == c.SubDomain {
|
|
|
|
return true
|
|
|
|
}
|
2018-06-26 12:28:47 +01:00
|
|
|
|
2019-01-22 10:50:55 +00:00
|
|
|
return false
|
2018-06-26 12:28:47 +01:00
|
|
|
}
|
2019-04-12 16:12:13 +01:00
|
|
|
|
2019-04-23 18:26:56 +01:00
|
|
|
func (c *CookieDomain) UnmarshalFlag(value string) error {
|
|
|
|
*c = *NewCookieDomain(value)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *CookieDomain) MarshalFlag() (string, error) {
|
|
|
|
return c.Domain, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Legacy support for comma separated list of cookie domains
|
|
|
|
|
2019-04-12 16:12:13 +01:00
|
|
|
type CookieDomains []CookieDomain
|
|
|
|
|
|
|
|
func (c *CookieDomains) UnmarshalFlag(value string) error {
|
|
|
|
if len(value) > 0 {
|
|
|
|
for _, d := range strings.Split(value, ",") {
|
|
|
|
cookieDomain := NewCookieDomain(d)
|
|
|
|
*c = append(*c, *cookieDomain)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *CookieDomains) MarshalFlag() (string, error) {
|
2019-04-18 15:07:39 +01:00
|
|
|
var domains []string
|
|
|
|
for _, d := range *c {
|
|
|
|
domains = append(domains, d.Domain)
|
|
|
|
}
|
|
|
|
return strings.Join(domains, ","), nil
|
2019-04-12 16:12:13 +01:00
|
|
|
}
|