Skip to content

Add support to forward header with oidc token #100

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ traefik.toml:

[entryPoints.http.auth.forward]
address = "http://traefik-forward-auth:4181"
authResponseHeaders = ["X-Forwarded-User"]
authResponseHeaders = ["X-Forwarded-User", "X-Oidc-Token"]

[docker]
endpoint = "unix:///var/run/docker.sock"
Expand Down Expand Up @@ -320,6 +320,9 @@ Note, if you pass `whitelist` then only this is checked and `domain` is effectiv

The authenticated user is set in the `X-Forwarded-User` header, to pass this on add this to the `authResponseHeaders` config option in traefik, as shown [here](https://github.com/thomseddon/traefik-forward-auth/blob/master/examples/docker-compose-dev.yml).

The OIDC/JWT token is set in the `X-Oidc-Token` header, to pass this on add this to the `authResponseHeaders` config option in traefik; the value is encrypt,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo: encrypted*

and need to be decrypted.

### Operation Modes

#### Overlay Mode
Expand Down
2 changes: 1 addition & 1 deletion examples/traefik.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@

[entryPoints.http.auth.forward]
address = "http://traefik-forward-auth:4181"
authResponseHeaders = ["X-Forwarded-User"]
authResponseHeaders = ["X-Forwarded-User", "X-Oidc-Token"]

################################################################
# Traefik logs configuration
Expand Down
23 changes: 12 additions & 11 deletions internal/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,41 +18,41 @@ import (
// Request Validation

// Cookie = hash(secret, cookie domain, email, expires)|expires|email
func ValidateCookie(r *http.Request, c *http.Cookie) (string, error) {
func ValidateCookie(r *http.Request, c *http.Cookie) (string, string, error ) {
parts := strings.Split(c.Value, "|")

if len(parts) != 3 {
return "", errors.New("Invalid cookie format")
if len(parts) != 4 {
return "", "", errors.New("Invalid cookie format")
}

mac, err := base64.URLEncoding.DecodeString(parts[0])
if err != nil {
return "", errors.New("Unable to decode cookie mac")
return "", "", errors.New("Unable to decode cookie mac")
}

expectedSignature := cookieSignature(r, parts[2], parts[1])
expected, err := base64.URLEncoding.DecodeString(expectedSignature)
if err != nil {
return "", errors.New("Unable to generate mac")
return "", "", errors.New("Unable to generate mac")
}

// Valid token?
if !hmac.Equal(mac, expected) {
return "", errors.New("Invalid cookie mac")
return "", "", errors.New("Invalid cookie mac")
}

expires, err := strconv.ParseInt(parts[1], 10, 64)
if err != nil {
return "", errors.New("Unable to parse cookie expiry")
return "", "", errors.New("Unable to parse cookie expiry")
}

// Has it expired?
if time.Unix(expires, 0).Before(time.Now()) {
return "", errors.New("Cookie has expired")
return "", "", errors.New("Cookie has expired")
}

// Looks valid
return parts[2], nil
return parts[2], parts[3], nil
}

// Validate email
Expand Down Expand Up @@ -127,10 +127,10 @@ func useAuthDomain(r *http.Request) (bool, string) {
// Cookie methods

// Create an auth cookie
func MakeCookie(r *http.Request, email string) *http.Cookie {
func MakeCookie(r *http.Request, email string, token string) *http.Cookie {
expires := cookieExpiry()
mac := cookieSignature(r, email, fmt.Sprintf("%d", expires.Unix()))
value := fmt.Sprintf("%s|%d|%s", mac, expires.Unix(), email)
value := fmt.Sprintf("%s|%d|%s|%s", mac, expires.Unix(), email, token)

return &http.Cookie{
Name: config.CookieName,
Expand All @@ -143,6 +143,7 @@ func MakeCookie(r *http.Request, email string) *http.Cookie {
}
}


// Make a CSRF cookie (used during login only)
func MakeCSRFCookie(r *http.Request, nonce string) *http.Cookie {
return &http.Cookie{
Expand Down
28 changes: 14 additions & 14 deletions internal/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,40 +24,40 @@ func TestAuthValidateCookie(t *testing.T) {

// Should require 3 parts
c.Value = ""
_, err := ValidateCookie(r, c)
_, _, err := ValidateCookie(r, c)
if assert.Error(err) {
assert.Equal("Invalid cookie format", err.Error())
}
c.Value = "1|2"
_, err = ValidateCookie(r, c)
_, _, err = ValidateCookie(r, c)
if assert.Error(err) {
assert.Equal("Invalid cookie format", err.Error())
}
c.Value = "1|2|3|4"
_, err = ValidateCookie(r, c)
c.Value = "1|2|3|4|5"
_, _, err = ValidateCookie(r, c)
if assert.Error(err) {
assert.Equal("Invalid cookie format", err.Error())
}

// Should catch invalid mac
c.Value = "MQ==|2|3"
_, err = ValidateCookie(r, c)
c.Value = "MQ==|2|3|4"
_, _, err = ValidateCookie(r, c)
if assert.Error(err) {
assert.Equal("Invalid cookie mac", err.Error())
}

// Should catch expired
config.Lifetime = time.Second * time.Duration(-1)
c = MakeCookie(r, "[email protected]")
_, err = ValidateCookie(r, c)
c = MakeCookie(r, "[email protected]", "token-value")
_, _, err = ValidateCookie(r, c)
if assert.Error(err) {
assert.Equal("Cookie has expired", err.Error())
}

// Should accept valid cookie
config.Lifetime = time.Second * time.Duration(10)
c = MakeCookie(r, "[email protected]")
email, err := ValidateCookie(r, c)
c = MakeCookie(r, "[email protected]", "token-value")
email, _, err := ValidateCookie(r, c)
assert.Nil(err, "valid request should not return an error")
assert.Equal("[email protected]", email, "valid request should return user email")
}
Expand Down Expand Up @@ -165,11 +165,11 @@ func TestAuthMakeCookie(t *testing.T) {
r, _ := http.NewRequest("GET", "http://app.example.com", nil)
r.Header.Add("X-Forwarded-Host", "app.example.com")

c := MakeCookie(r, "[email protected]")
c := MakeCookie(r, "[email protected]", "token-value")
assert.Equal("_forward_auth", c.Name)
parts := strings.Split(c.Value, "|")
assert.Len(parts, 3, "cookie should be 3 parts")
_, err := ValidateCookie(r, c)
assert.Len(parts, 4, "cookie should be 4 parts")
_, _, err := ValidateCookie(r, c)
assert.Nil(err, "should generate valid cookie")
assert.Equal("/", c.Path)
assert.Equal("app.example.com", c.Domain)
Expand All @@ -180,7 +180,7 @@ func TestAuthMakeCookie(t *testing.T) {

config.CookieName = "testname"
config.InsecureCookie = true
c = MakeCookie(r, "[email protected]")
c = MakeCookie(r, "[email protected]", "token-value")
assert.Equal("testname", c.Name)
assert.False(c.Secure)
}
Expand Down
7 changes: 4 additions & 3 deletions internal/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ func (s *Server) AuthHandler(providerName, rule string) http.HandlerFunc {
}

// Validate cookie
email, err := ValidateCookie(r, c)
email, oidc_token, err := ValidateCookie(r, c)
if err != nil {
if err.Error() == "Cookie has expired" {
logger.Info("Cookie has expired")
Expand All @@ -105,7 +105,8 @@ func (s *Server) AuthHandler(providerName, rule string) http.HandlerFunc {

// Valid request
logger.Debugf("Allowing valid request ")
w.Header().Set("X-Forwarded-User", email)
w.Header().Add("X-Forwarded-User", email)
w.Header().Add("X-Oidc-Token", oidc_token)
w.WriteHeader(200)
}
}
Expand Down Expand Up @@ -159,7 +160,7 @@ func (s *Server) AuthCallbackHandler() http.HandlerFunc {
}

// Generate cookie
http.SetCookie(w, MakeCookie(r, user.Email))
http.SetCookie(w, MakeCookie(r, user.Email, token))
logger.WithFields(logrus.Fields{
"user": user.Email,
}).Infof("Generated auth cookie")
Expand Down
24 changes: 20 additions & 4 deletions internal/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ func TestServerAuthHandlerInvalid(t *testing.T) {

// Should catch invalid cookie
req = newDefaultHttpRequest("/foo")
c := MakeCookie(req, "[email protected]")
c := MakeCookie(req, "[email protected]", "token-value")
parts = strings.Split(c.Value, "|")
c.Value = fmt.Sprintf("bad|%s|%s", parts[1], parts[2])

Expand All @@ -64,7 +64,7 @@ func TestServerAuthHandlerInvalid(t *testing.T) {

// Should validate email
req = newDefaultHttpRequest("/foo")
c = MakeCookie(req, "[email protected]")
c = MakeCookie(req, "[email protected]", "token-value")
config.Domains = []string{"test.com"}

res, _ = doHttpRequest(req, c)
Expand All @@ -79,7 +79,7 @@ func TestServerAuthHandlerExpired(t *testing.T) {

// Should redirect expired cookie
req := newDefaultHttpRequest("/foo")
c := MakeCookie(req, "[email protected]")
c := MakeCookie(req, "[email protected]", "token1")
res, _ := doHttpRequest(req, c)
assert.Equal(307, res.StatusCode, "request with expired cookie should be redirected")

Expand All @@ -105,7 +105,7 @@ func TestServerAuthHandlerValid(t *testing.T) {

// Should allow valid request email
req := newDefaultHttpRequest("/foo")
c := MakeCookie(req, "[email protected]")
c := MakeCookie(req, "[email protected]", "token1")
config.Domains = []string{}

res, _ := doHttpRequest(req, c)
Expand All @@ -117,6 +117,22 @@ func TestServerAuthHandlerValid(t *testing.T) {
assert.Equal([]string{"[email protected]"}, users, "X-Forwarded-User header should match user")
}

func TestServerForwardOIDCTokenWhenAuthHandlerValid(t *testing.T) {
assert := assert.New(t)
config = newDefaultConfig()

// Should allow valid request email
req := newDefaultHttpRequest("/foo")
c := MakeCookie(req, "[email protected]", "token1")
config.Domains = []string{}

res, _ := doHttpRequest(req, c)
assert.Equal(200, res.StatusCode, "valid request should be allowed")

oidc_token := res.Header["X-Oidc-Token"]
assert.Len(oidc_token, 1, res)
}

func TestServerAuthCallback(t *testing.T) {
assert := assert.New(t)
config = newDefaultConfig()
Expand Down