Skip to content
Snippets Groups Projects
Select Git revision
  • 57736a412bd3db10bddd4ef516ffe5887fe63ddb
  • master default
  • method_check
  • custom_prefix
  • package
  • cookies
  • v2.1.1
  • v2.1.0
  • v2.1.0-rc5
  • v2.1.0-rc4
  • v2.1.0-rc3
  • v2.1.0-rc2
  • v2.1.0-rc1
  • v2.0.7
  • v2.0.6
  • v2.0.5
  • v2.0.4
  • v2.0.3
  • v2.0.2
  • v2.0.1
  • v2.0.0
  • v1.2.8
  • v1.2.7
  • v1.2.6
  • v1.2.5
  • v1.2.4
26 results

server_test.go

  • server_test.go 14.75 KiB
    /*
    Copyright 2015 All rights reserved.
    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
    You may obtain a copy of the License at
    
        http://www.apache.org/licenses/LICENSE-2.0
    
    Unless required by applicable law or agreed to in writing, software
    distributed under the License is distributed on an "AS IS" BASIS,
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    See the License for the specific language governing permissions and
    limitations under the License.
    */
    
    package main
    
    import (
    	"encoding/json"
    	"errors"
    	"fmt"
    	"net/http"
    	"net/http/httptest"
    	"net/url"
    	"strings"
    	"testing"
    	"time"
    
    	"github.com/gambol99/go-oidc/jose"
    	"github.com/stretchr/testify/assert"
    )
    
    const (
    	fakeAdminRole          = "role:admin"
    	fakeAdminRoleURL       = "/admin*"
    	fakeAuthAllURL         = "/auth_all/*"
    	fakeClientID           = "test"
    	fakeSecret             = "test"
    	fakeTestAdminRolesURL  = "/test_admin_roles"
    	fakeTestRole           = "role:test"
    	fakeTestRoleURL        = "/test_role"
    	fakeTestWhitelistedURL = "/auth_all/white_listed*"
    	testProxyAccepted      = "Proxy-Accepted"
    	validUsername          = "test"
    	validPassword          = "test"
    )
    
    var (
    	defaultTestTokenClaims = jose.Claims{
    		"aud":                "test",
    		"azp":                "clientid",
    		"client_session":     "f0105893-369a-46bc-9661-ad8c747b1a69",
    		"email":              "gambol99@gmail.com",
    		"family_name":        "Jayawardene",
    		"given_name":         "Rohith",
    		"iat":                "1450372669",
    		"iss":                "test",
    		"jti":                "4ee75b8e-3ee6-4382-92d4-3390b4b4937b",
    		"name":               "Rohith Jayawardene",
    		"nbf":                0,
    		"preferred_username": "rjayawardene",
    		"session_state":      "98f4c3d2-1b8c-4932-b8c4-92ec0ea7e195",
    		"sub":                "1e11e539-8256-4b3b-bda8-cc0d56cddb48",
    		"typ":                "Bearer",
    	}
    )
    
    func TestNewKeycloakProxy(t *testing.T) {
    	cfg := newFakeKeycloakConfig()
    	cfg.DiscoveryURL = newFakeAuthServer().getLocation()
    	cfg.Listen = "127.0.0.1:0"
    	cfg.ListenHTTP = ""
    
    	proxy, err := newProxy(cfg)
    	assert.NoError(t, err)
    	assert.NotNil(t, proxy)
    	assert.NotNil(t, proxy.config)
    	assert.NotNil(t, proxy.router)
    	assert.NotNil(t, proxy.endpoint)
    	assert.NoError(t, proxy.Run())
    }
    
    func TestReverseProxyHeaders(t *testing.T) {
    	p := newFakeProxy(nil)
    	token := newTestToken(p.idp.getLocation())
    	token.addRealmRoles([]string{fakeAdminRole})
    	signed, _ := p.idp.signToken(token.claims)
    	requests := []fakeRequest{
    		{
    			URI:           "/auth_all/test",
    			RawToken:      signed.Encode(),
    			ExpectedProxy: true,
    			ExpectedProxyHeaders: map[string]string{
    				"X-Auth-Email":    "gambol99@gmail.com",
    				"X-Auth-Roles":    "role:admin",
    				"X-Auth-Subject":  token.claims["sub"].(string),
    				"X-Auth-Token":    signed.Encode(),
    				"X-Auth-Userid":   "rjayawardene",
    				"X-Auth-Username": "rjayawardene",
    			},
    			ExpectedCode: http.StatusOK,
    		},
    	}
    	p.RunTests(t, requests)
    }
    
    func TestForwardingProxy(t *testing.T) {
    	cfg := newFakeKeycloakConfig()
    	cfg.EnableForwarding = true
    	cfg.ForwardingDomains = []string{}
    	cfg.ForwardingUsername = "test"
    	cfg.ForwardingPassword = "test"
    	s := httptest.NewServer(&fakeUpstreamService{})
    	requests := []fakeRequest{
    		{
    			URL:                     s.URL + "/test",
    			ProxyRequest:            true,
    			ExpectedProxy:           true,
    			ExpectedCode:            http.StatusOK,
    			ExpectedContentContains: "Bearer ey",
    		},
    	}
    	p := newFakeProxy(cfg)
    	<-time.After(time.Duration(100) * time.Millisecond)
    	p.RunTests(t, requests)
    }
    
    func TestForbiddenTemplate(t *testing.T) {
    	cfg := newFakeKeycloakConfig()
    	cfg.ForbiddenPage = "templates/forbidden.html.tmpl"
    	cfg.Resources = []*Resource{
    		{
    			URL:     "/*",
    			Methods: allHTTPMethods,
    			Roles:   []string{fakeAdminRole},
    		},
    	}
    	requests := []fakeRequest{
    		{
    			URI:                     "/test",
    			Redirects:               false,
    			HasToken:                true,
    			ExpectedCode:            http.StatusForbidden,
    			ExpectedContentContains: "403 Permission Denied",
    		},
    	}
    	newFakeProxy(cfg).RunTests(t, requests)
    }
    
    func TestAudienceHeader(t *testing.T) {
    	c := newFakeKeycloakConfig()
    	c.NoRedirects = false
    	requests := []fakeRequest{
    		{
    			URI:           "/auth_all/test",
    			HasLogin:      true,
    			ExpectedProxy: true,
    			Redirects:     true,
    			ExpectedProxyHeaders: map[string]string{
    				"X-Auth-Audience": "test",
    			},
    			ExpectedCode: http.StatusOK,
    		},
    	}
    	newFakeProxy(c).RunTests(t, requests)
    }
    
    func TestAuthorizationTemplate(t *testing.T) {
    	cfg := newFakeKeycloakConfig()
    	cfg.SignInPage = "templates/sign_in.html.tmpl"
    	cfg.Resources = []*Resource{
    		{
    			URL:     "/*",
    			Methods: allHTTPMethods,
    			Roles:   []string{fakeAdminRole},
    		},
    	}
    	requests := []fakeRequest{
    		{
    			URI:                     oauthURL + authorizationURL,
    			Redirects:               true,
    			ExpectedCode:            http.StatusOK,
    			ExpectedContentContains: "Sign In",
    		},
    	}
    	newFakeProxy(cfg).RunTests(t, requests)
    }
    
    func TestProxyProtocol(t *testing.T) {
    	c := newFakeKeycloakConfig()
    	c.EnableProxyProtocol = true
    	requests := []fakeRequest{
    		{
    			URI:           fakeAuthAllURL + "/test",
    			HasToken:      true,
    			ExpectedProxy: true,
    			ExpectedProxyHeaders: map[string]string{
    				"X-Forwarded-For": "127.0.0.1",
    			},
    			ExpectedCode: http.StatusOK,
    		},
    		{
    			URI:           fakeAuthAllURL + "/test",
    			HasToken:      true,
    			ProxyProtocol: "189.10.10.1",
    			ExpectedProxy: true,
    			ExpectedProxyHeaders: map[string]string{
    				"X-Forwarded-For": "189.10.10.1",
    			},
    			ExpectedCode: http.StatusOK,
    		},
    	}
    	newFakeProxy(c).RunTests(t, requests)
    }
    
    func TestTokenEncryption(t *testing.T) {
    	c := newFakeKeycloakConfig()
    	c.EnableEncryptedToken = true
    	c.EncryptionKey = "US36S5kubc4BXbfzCIKTQcTzG6lvixVv"
    	requests := []fakeRequest{
    		{
    			URI:           "/auth_all/test",
    			HasLogin:      true,
    			ExpectedProxy: true,
    			Redirects:     true,
    			ExpectedProxyHeaders: map[string]string{
    				"X-Auth-Email":    "gambol99@gmail.com",
    				"X-Auth-Userid":   "rjayawardene",
    				"X-Auth-Username": "rjayawardene",
    				"X-Forwarded-For": "127.0.0.1",
    			},
    			ExpectedCode: http.StatusOK,
    		},
    		// the token must be encrypted
    		{
    			URI:          "/auth_all/test",
    			HasToken:     true,
    			ExpectedCode: http.StatusUnauthorized,
    		},
    	}
    	newFakeProxy(c).RunTests(t, requests)
    }
    
    func TestSkipClientIDDisabled(t *testing.T) {
    	c := newFakeKeycloakConfig()
    	p := newFakeProxy(c)
    	// create two token, one with a bad client id
    	bad := newTestToken(p.idp.getLocation())
    	bad.merge(jose.Claims{"aud": "bad_client_id"})
    	badSigned, _ := p.idp.signToken(bad.claims)
    	// and the good
    	good := newTestToken(p.idp.getLocation())
    	goodSigned, _ := p.idp.signToken(good.claims)
    	requests := []fakeRequest{
    		{
    			URI:           "/auth_all/test",
    			RawToken:      goodSigned.Encode(),
    			ExpectedProxy: true,
    			ExpectedCode:  http.StatusOK,
    		},
    		{
    			URI:          "/auth_all/test",
    			RawToken:     badSigned.Encode(),
    			ExpectedCode: http.StatusForbidden,
    		},
    	}
    	p.RunTests(t, requests)
    }
    
    func TestSkipClientIDEnabled(t *testing.T) {
    	c := newFakeKeycloakConfig()
    	c.SkipClientID = true
    	p := newFakeProxy(c)
    	// create two token, one with a bad client id
    	bad := newTestToken(p.idp.getLocation())
    	bad.merge(jose.Claims{"aud": "bad_client_id"})
    	badSigned, _ := p.idp.signToken(bad.claims)
    	// and the good
    	good := newTestToken(p.idp.getLocation())
    	goodSigned, _ := p.idp.signToken(good.claims)
    	// bad issuer
    	badIssurer := newTestToken("http://someone_else")
    	badIssurer.merge(jose.Claims{"aud": "bad_client_id"})
    	badIssuerSigned, _ := p.idp.signToken(badIssurer.claims)
    
    	requests := []fakeRequest{
    		{
    			URI:           "/auth_all/test",
    			RawToken:      goodSigned.Encode(),
    			ExpectedProxy: true,
    			ExpectedCode:  http.StatusOK,
    		},
    		{
    			URI:           "/auth_all/test",
    			RawToken:      badSigned.Encode(),
    			ExpectedProxy: true,
    			ExpectedCode:  http.StatusOK,
    		},
    		{
    			URI:          "/auth_all/test",
    			RawToken:     badIssuerSigned.Encode(),
    			ExpectedCode: http.StatusForbidden,
    		},
    	}
    	p.RunTests(t, requests)
    }
    
    func TestAuthTokenHeaderEnabled(t *testing.T) {
    	p := newFakeProxy(nil)
    	token := newTestToken(p.idp.getLocation())
    	signed, _ := p.idp.signToken(token.claims)
    
    	requests := []fakeRequest{
    		{
    			URI:      "/auth_all/test",
    			RawToken: signed.Encode(),
    			ExpectedProxyHeaders: map[string]string{
    				"X-Auth-Token": signed.Encode(),
    			},
    			ExpectedProxy: true,
    			ExpectedCode:  http.StatusOK,
    		},
    	}
    	p.RunTests(t, requests)
    }
    
    func TestAuthTokenHeaderDisabled(t *testing.T) {
    	c := newFakeKeycloakConfig()
    	c.EnableTokenHeader = false
    	p := newFakeProxy(c)
    	token := newTestToken(p.idp.getLocation())
    	signed, _ := p.idp.signToken(token.claims)
    
    	requests := []fakeRequest{
    		{
    			URI:      "/auth_all/test",
    			RawToken: signed.Encode(),
    			ExpectedProxyHeaders: map[string]string{
    				"X-Auth-Token": "",
    			},
    			ExpectedProxy: true,
    			ExpectedCode:  http.StatusOK,
    		},
    	}
    	p.RunTests(t, requests)
    }
    
    func TestDisableAuthorizationCookie(t *testing.T) {
    	c := newFakeKeycloakConfig()
    	c.EnableAuthorizationCookies = false
    	p := newFakeProxy(c)
    	token := newTestToken(p.idp.getLocation())
    	signed, _ := p.idp.signToken(token.claims)
    
    	requests := []fakeRequest{
    		{
    			URI: "/auth_all/test",
    			Cookies: []*http.Cookie{
    				{Name: c.CookieAccessName, Value: signed.Encode()},
    				{Name: "mycookie", Value: "myvalue"},
    			},
    			HasToken:                true,
    			ExpectedContentContains: "kc-access=censored; mycookie=myvalue",
    			ExpectedCode:            http.StatusOK,
    			ExpectedProxy:           true,
    		},
    	}
    	p.RunTests(t, requests)
    }
    
    func newTestService() string {
    	_, _, u := newTestProxyService(nil)
    	return u
    }
    
    func newTestProxyService(config *Config) (*oauthProxy, *fakeAuthServer, string) {
    	auth := newFakeAuthServer()
    	if config == nil {
    		config = newFakeKeycloakConfig()
    	}
    	config.DiscoveryURL = auth.getLocation()
    	config.RevocationEndpoint = auth.getRevocationURL()
    	config.Verbose = false
    	config.EnableLogging = false
    
    	proxy, err := newProxy(config)
    	if err != nil {
    		panic("failed to create proxy service, error: " + err.Error())
    	}
    
    	// step: create an fake upstream endpoint
    	proxy.upstream = new(fakeUpstreamService)
    	service := httptest.NewServer(proxy.router)
    	config.RedirectionURL = service.URL
    
    	// step: we need to update the client config
    	if proxy.client, proxy.idp, proxy.idpClient, err = proxy.newOpenIDClient(); err != nil {
    		panic("failed to recreate the openid client, error: " + err.Error())
    	}
    
    	return proxy, auth, service.URL
    }
    
    func newFakeHTTPRequest(method, path string) *http.Request {
    	return &http.Request{
    		Method: method,
    		Header: make(map[string][]string),
    		Host:   "127.0.0.1",
    		URL: &url.URL{
    			Scheme: "http",
    			Host:   "127.0.0.1",
    			Path:   path,
    		},
    	}
    }
    
    func newFakeKeycloakConfig() *Config {
    	return &Config{
    		ClientID:                   fakeClientID,
    		ClientSecret:               fakeSecret,
    		CookieAccessName:           "kc-access",
    		CookieRefreshName:          "kc-state",
    		DisableAllLogging:          true,
    		DiscoveryURL:               "127.0.0.1:0",
    		OpenIDProviderTimeout:      time.Second * 5,
    		EnableAuthorizationHeader:  true,
    		EnableAuthorizationCookies: true,
    		EnableLogging:              false,
    		EnableLoginHandler:         true,
    		EnableTokenHeader:          true,
    		Listen:                     "127.0.0.1:0",
    		Scopes:                     []string{},
    		Verbose:                    true,
    		Resources: []*Resource{
    			{
    				URL:     fakeAdminRoleURL,
    				Methods: []string{"GET"},
    				Roles:   []string{fakeAdminRole},
    			},
    			{
    				URL:     fakeTestRoleURL,
    				Methods: []string{"GET"},
    				Roles:   []string{fakeTestRole},
    			},
    			{
    				URL:     fakeTestAdminRolesURL,
    				Methods: []string{"GET"},
    				Roles:   []string{fakeAdminRole, fakeTestRole},
    			},
    			{
    				URL:     fakeAuthAllURL,
    				Methods: allHTTPMethods,
    				Roles:   []string{},
    			},
    			{
    				URL:         fakeTestWhitelistedURL,
    				WhiteListed: true,
    				Methods:     allHTTPMethods,
    				Roles:       []string{},
    			},
    		},
    	}
    }
    
    func makeTestCodeFlowLogin(location string) (*http.Response, error) {
    	u, err := url.Parse(location)
    	if err != nil {
    		return nil, err
    	}
    	// step: get the redirect
    	var resp *http.Response
    	for count := 0; count < 4; count++ {
    		req, err := http.NewRequest(http.MethodGet, location, nil)
    		if err != nil {
    			return nil, err
    		}
    		// step: make the request
    		resp, err = http.DefaultTransport.RoundTrip(req)
    		if err != nil {
    			return nil, err
    		}
    		if resp.StatusCode != http.StatusTemporaryRedirect {
    			return nil, errors.New("no redirection found in resp")
    		}
    		location = resp.Header.Get("Location")
    		if !strings.HasPrefix(location, "http") {
    			location = fmt.Sprintf("%s://%s%s", u.Scheme, u.Host, location)
    		}
    	}
    	return resp, nil
    }
    
    // fakeUpstreamResponse is the response from fake upstream
    type fakeUpstreamResponse struct {
    	URI     string      `json:"uri"`
    	Method  string      `json:"method"`
    	Address string      `json:"address"`
    	Headers http.Header `json:"headers"`
    }
    
    // fakeUpstreamService acts as a fake upstream service, returns the headers and request
    type fakeUpstreamService struct{}
    
    func (f *fakeUpstreamService) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    	w.Header().Set(testProxyAccepted, "true")
    	w.Header().Set("Content-Type", "application/json")
    	w.WriteHeader(http.StatusOK)
    	content, _ := json.Marshal(&fakeUpstreamResponse{
    		URI:     r.RequestURI,
    		Method:  r.Method,
    		Address: r.RemoteAddr,
    		Headers: r.Header,
    	})
    	w.Write(content)
    }
    
    type fakeToken struct {
    	claims jose.Claims
    }
    
    func newTestToken(issuer string) *fakeToken {
    	claims := make(jose.Claims)
    	for k, v := range defaultTestTokenClaims {
    		claims[k] = v
    	}
    	claims.Add("exp", float64(time.Now().Add(1*time.Hour).Unix()))
    	claims.Add("iat", float64(time.Now().Unix()))
    	claims.Add("iss", issuer)
    
    	return &fakeToken{claims: claims}
    }
    
    // merge is responsible for merging claims into the token
    func (t *fakeToken) merge(claims jose.Claims) {
    	for k, v := range claims {
    		t.claims.Add(k, v)
    	}
    }
    
    // getToken returns a JWT token from the clains
    func (t *fakeToken) getToken() jose.JWT {
    	tk, _ := jose.NewJWT(jose.JOSEHeader{"alg": "RS256"}, t.claims)
    	return tk
    }
    
    // setExpiration sets the expiration of the token
    func (t *fakeToken) setExpiration(tm time.Time) {
    	t.claims.Add("exp", float64(tm.Unix()))
    }
    
    // addGroups adds groups to then token
    func (t *fakeToken) addGroups(groups []string) {
    	t.claims.Add("groups", groups)
    }
    
    // addRealmRoles adds realms roles to token
    func (t *fakeToken) addRealmRoles(roles []string) {
    	t.claims.Add("realm_access", map[string]interface{}{
    		"roles": roles,
    	})
    }
    
    // addClientRoles adds client roles to the token
    func (t *fakeToken) addClientRoles(client string, roles []string) {
    	t.claims.Add("resource_access", map[string]interface{}{
    		client: map[string]interface{}{
    			"roles": roles,
    		},
    	})
    }