Skip to content
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
2 changes: 1 addition & 1 deletion .github/workflows/testing.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,4 @@ jobs:

- name: Test with the Go CLI
working-directory: ./src
run: go test
run: go test -race ./...
2 changes: 2 additions & 0 deletions src/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"net/url"
"os"
"strings"
"time"

"github.com/golang-jwt/jwt/v5"

Expand Down Expand Up @@ -277,6 +278,7 @@ func New(uctx context.Context, next http.Handler, cfg *config.Config, name strin

httpClient := &http.Client{
Transport: httpTransport,
Timeout: 30 * time.Second,
}

logger.Log(logging.LevelInfo, "Configuration loaded successfully, starting OIDC Auth middleware...")
Expand Down
18 changes: 17 additions & 1 deletion src/oidc.go
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,12 @@ func (toa *TraefikOidcAuth) introspectToken(token string) (bool, map[string]inte

defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
toa.logger.Log(logging.LevelError, "introspectToken: received bad HTTP response from Provider (Status: %d): %s", resp.StatusCode, string(body))
return false, nil, errors.New("invalid status code")
}

var introspectResponse map[string]interface{}
err = json.NewDecoder(resp.Body).Decode(&introspectResponse)

Expand All @@ -255,13 +261,23 @@ func (toa *TraefikOidcAuth) renewToken(refreshToken string) (*oidc.OidcTokenResp
"client_id": {toa.Config.Provider.ClientId},
"scope": {strings.Join(toa.Config.Scopes, " ")},
"refresh_token": {refreshToken},
"resources": toa.Config.RequestedResources,
"resource": toa.Config.RequestedResources,
}

if toa.Config.Provider.ClientSecret != "" {
urlValues.Add("client_secret", toa.Config.Provider.ClientSecret)
}

if toa.ClientJwtPrivateKey != nil {
clientAssertionToken, err := toa.getClientAssertionJwtToken()
if err != nil {
return nil, err
}

urlValues.Add("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer")
urlValues.Add("client_assertion", clientAssertionToken)
}

resp, err := toa.httpClient.PostForm(toa.DiscoveryDocument.TokenEndpoint, urlValues)

if err != nil {
Expand Down
6 changes: 6 additions & 0 deletions src/oidc/jwks.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,9 @@ func (h *JwksHandler) Keyfunc(token *jwt.Token) (any, error) {
}

func (h *JwksHandler) getRsaKey(kid string) (*rsa.PublicKey, error) {
h.Lock.RLock()
defer h.Lock.RUnlock()

k := h.findRsaKey(kid)

if k != nil {
Expand All @@ -158,6 +161,9 @@ func (h *JwksHandler) getRsaKey(kid string) (*rsa.PublicKey, error) {
return nil, errors.New("unknown kid " + kid)
}
func (h *JwksHandler) getEcdsaKey(kid string) (*ecdsa.PublicKey, error) {
h.Lock.RLock()
defer h.Lock.RUnlock()

k := h.findEcdsaKey(kid)

if k != nil {
Expand Down
65 changes: 65 additions & 0 deletions src/oidc/jwks_test.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
package oidc

import (
"encoding/json"
"net/http"
"net/http/httptest"
"sync"
"testing"
"time"

"github.com/golang-jwt/jwt/v5"
"github.com/sevensolutions/traefik-oidc-auth/src/logging"
)

func TestKeyfunc_MissingOrInvalidKid(t *testing.T) {
Expand Down Expand Up @@ -50,3 +56,62 @@ func TestKeyfunc_MissingOrInvalidKid(t *testing.T) {
})
}
}

func TestKeyfunc_ConcurrentWithReload(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
rw.Header().Set("Content-Type", "application/json")
json.NewEncoder(rw).Encode(JwksKeys{
Keys: []JwksKey{
{
Kid: "test-key",
Kty: "RSA",
Use: "sig",
N: "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw",
E: "AQAB",
},
},
})
}))
defer server.Close()

h := &JwksHandler{Url: server.URL}
logger := logging.CreateLogger(logging.LevelError)

if err := h.EnsureLoaded(logger, server.Client(), false); err != nil {
t.Fatal(err)
}

token := &jwt.Token{
Method: jwt.SigningMethodRS256,
Header: map[string]any{"kid": "test-key"},
}

var wg sync.WaitGroup

for i := 0; i < 8; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for j := 0; j < 100; j++ {
h.Keyfunc(token)
}
}()
}

wg.Add(1)
go func() {
defer wg.Done()
for j := 0; j < 100; j++ {
h.Lock.Lock()
h.CacheDate = time.Time{}
h.Lock.Unlock()

if err := h.EnsureLoaded(logger, server.Client(), false); err != nil {
t.Error(err)
return
}
}
}()

wg.Wait()
}
78 changes: 78 additions & 0 deletions src/oidc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import (
"math/big"
"net/http"
"net/http/httptest"
"net/url"
"sync"
"testing"

"github.com/golang-jwt/jwt/v5"
Expand Down Expand Up @@ -454,3 +456,79 @@ func setupJWKS(t *testing.T, toa *TraefikOidcAuth, privateKey *rsa.PrivateKey) *
toa.Jwks.Url = jwksServer.URL
return jwksServer
}

func TestIntrospectToken_BadStatusCode(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
http.Error(rw, `{"active":true}`, http.StatusUnauthorized)
}))
defer server.Close()

toa := &TraefikOidcAuth{
logger: logging.CreateLogger(logging.LevelError),
httpClient: server.Client(),
Config: &config.Config{Provider: &config.ProviderConfig{}},
DiscoveryDocument: &oidc.OidcDiscovery{IntrospectionEndpoint: server.URL},
}

active, _, err := toa.introspectToken("some-token")

if active || err == nil {
t.Errorf("expected a failed introspection request to be an error, got active=%v err=%v", active, err)
}
}

func TestRenewToken_SendsResourceAndClientAssertion(t *testing.T) {
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
t.Fatal(err)
}

var mu sync.Mutex
var form url.Values

server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
if err := req.ParseForm(); err != nil {
t.Error(err)
return
}

mu.Lock()
form = req.PostForm
mu.Unlock()

rw.Header().Set("Content-Type", "application/json")
json.NewEncoder(rw).Encode(oidc.OidcTokenResponse{AccessToken: "new-access-token"})
}))
defer server.Close()

toa := &TraefikOidcAuth{
logger: logging.CreateLogger(logging.LevelError),
httpClient: server.Client(),
Config: &config.Config{
Provider: &config.ProviderConfig{ClientId: "my-client", ClientJwtPrivateKeyId: "my-key-id"},
Scopes: []string{"openid"},
RequestedResources: []string{"https://api.example.com"},
},
ClientJwtPrivateKey: privateKey,
DiscoveryDocument: &oidc.OidcDiscovery{TokenEndpoint: server.URL},
}

if _, err := toa.renewToken("some-refresh-token"); err != nil {
t.Fatalf("expected no error, got %v", err)
}

mu.Lock()
defer mu.Unlock()

if got := form.Get("resource"); got != "https://api.example.com" {
t.Errorf("expected the requested resource to be sent as resource, got %q", got)
}

if form.Get("client_assertion") == "" {
t.Error("expected a client assertion to be sent")
}

if got := form.Get("client_assertion_type"); got != "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" {
t.Errorf("unexpected client_assertion_type %q", got)
}
}
2 changes: 1 addition & 1 deletion taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ tasks:
desc: 'Run all go unit tests'
dir: 'src'
cmds:
- go test
- go test -race ./...
test:e2e:
desc: 'Run all end to end tests'
dir: 'e2e'
Expand Down
Loading