Skip to content

Commit

Permalink
Merge pull request #165 from greenbone/AT-521-enable-authentication-f…
Browse files Browse the repository at this point in the history
…or-opensearch

At 521 enable authentication for opensearch
  • Loading branch information
larox11 authored Jun 5, 2024
2 parents 2ab8e6c + 54fb413 commit cbf5d5d
Show file tree
Hide file tree
Showing 9 changed files with 292 additions and 8 deletions.
1 change: 0 additions & 1 deletion .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,3 @@ linters:
- goimports
- gosec
- errorlint
- wrapcheck
2 changes: 1 addition & 1 deletion auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import (
"strings"

"github.com/Nerzal/gocloak/v12"
"github.com/golang-jwt/jwt/v4"
"github.com/golang-jwt/jwt/v5"
)

// KeycloakAuthorizer is used to validate if JWT has a correct signature and is valid and returns keycloak claims
Expand Down
2 changes: 1 addition & 1 deletion auth/example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import (

"github.com/Nerzal/gocloak/v12"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v4"
"github.com/golang-jwt/jwt/v5"
"github.com/jarcoal/httpmock"
"github.com/samber/lo"

Expand Down
2 changes: 1 addition & 1 deletion auth/jwt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import (
"testing"

"github.com/Nerzal/gocloak/v12"
"github.com/golang-jwt/jwt/v4"
"github.com/golang-jwt/jwt/v5"
"github.com/jarcoal/httpmock"
"github.com/samber/lo"
"github.com/stretchr/testify/require"
Expand Down
76 changes: 76 additions & 0 deletions client/keycloakJWTReceiverCachedInMemory.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// SPDX-FileCopyrightText: 2024 Greenbone AG
//
// SPDX-License-Identifier: GPL-3.0-or-later

package client

import (
"fmt"
"sync"

"github.com/Nerzal/gocloak/v12"
"github.com/golang-jwt/jwt/v5"
"github.com/rs/zerolog/log"
)

type KeycloakJWTReceiverCachedInMemory struct {
keycloakRepository IKeycloakRepository
mutex sync.Mutex
cachedToken *gocloak.JWT
}

func NewKeycloakJWTReceiverCachedInMemory(keycloakRepository IKeycloakRepository) *KeycloakJWTReceiverCachedInMemory {
return &KeycloakJWTReceiverCachedInMemory{
keycloakRepository: keycloakRepository,
}
}

func isTokenValid(token *gocloak.JWT) bool {
if token == nil {
return false
}

parser := jwt.NewParser()
claims := &jwt.MapClaims{}

_, _, err := parser.ParseUnverified(token.AccessToken, claims)
if err != nil {
log.Error().Msgf("couldn't parse JWT access token: %v", err)
return false
}

err = jwt.NewValidator(
jwt.WithIssuedAt(),
jwt.WithExpirationRequired(),
).Validate(claims)
if err != nil {
log.Debug().Msgf("JWT access token is invalid: %v", err)
return false
}

return true
}

func (k *KeycloakJWTReceiverCachedInMemory) getClientToken(clientName, clientSecret string) (*gocloak.JWT, error) {
k.mutex.Lock()
defer k.mutex.Unlock()

if k.cachedToken == nil || !isTokenValid(k.cachedToken) {
token, err := k.keycloakRepository.getClientToken(clientName, clientSecret)
if err != nil {
return nil, fmt.Errorf("couldn't fetch JWT access token: %w", err)
}
k.cachedToken = token
}

return k.cachedToken, nil
}

func (k *KeycloakJWTReceiverCachedInMemory) GetClientAccessToken(clientName, clientSecret string) (string, error) {
token, err := k.getClientToken(clientName, clientSecret)
if err != nil {
return "", err
}

return token.AccessToken, nil
}
157 changes: 157 additions & 0 deletions client/keycloakJWTReceiverCachedInMemory_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
// SPDX-FileCopyrightText: 2024 Greenbone AG
//
// SPDX-License-Identifier: GPL-3.0-or-later

package client

import (
"testing"

"github.com/Nerzal/gocloak/v12"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)

type MockKeycloakRepository struct {
mock.Mock
}

func (m *MockKeycloakRepository) getClientToken(clientName, clientSecret string) (*gocloak.JWT, error) {
args := m.Called()
return args.Get(0).(*gocloak.JWT), args.Error(1)
}

func TestKeycloakJWTReceiverCachedInMemory_GetClientToken(t *testing.T) {

testCases := []struct {
name string
cachedToken *gocloak.JWT
mockToken *gocloak.JWT
mockError error
expectedToken *gocloak.JWT
expectedError error
shouldFetchToken bool
}{
{
name: "No cached token",
cachedToken: nil,
mockToken: &gocloak.JWT{
AccessToken: "test_token",
},
expectedToken: &gocloak.JWT{
AccessToken: "test_token",
},
expectedError: nil,
shouldFetchToken: true,
},
{
name: "invalid cached token",
cachedToken: &gocloak.JWT{
AccessToken: "not a valid token",
},
mockToken: &gocloak.JWT{
AccessToken: "test_token",
},
expectedToken: &gocloak.JWT{
AccessToken: "test_token",
},
expectedError: nil,
shouldFetchToken: true,
},
{
name: "Expired cached token",
cachedToken: &gocloak.JWT{
AccessToken: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE1MTYyMzEwMjJ9.hsfQPY3ZVrVIV-bzI54NRoTDG6wWzORVp68lxGa3D08",
},
mockToken: &gocloak.JWT{
AccessToken: "test_token",
},
expectedToken: &gocloak.JWT{
AccessToken: "test_token",
},
expectedError: nil,
shouldFetchToken: true,
},
{
name: "NotBefore date is in the future",
cachedToken: &gocloak.JWT{
AccessToken: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwibmJmIjo0ODczMjQyNTg3LCJleHAiOjQ4NzQyNDI1ODd9.QZeQwoWl-HRbCcuZbt_3DFnA_h-zD5DhPmcBR0TyrQw",
},
mockToken: &gocloak.JWT{
AccessToken: "test_token",
},
expectedToken: &gocloak.JWT{
AccessToken: "test_token",
},
expectedError: nil,
shouldFetchToken: true,
},
{
name: "IssuedAt date is in the future",
cachedToken: &gocloak.JWT{
AccessToken: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0Ijo0ODczMjQyNTg3LCJleHAiOjQ4NzQyNDI1ODd9.h63qP0fMQGgx5S8eV-EHEO1zgSlBmjX3xR80iXnvhX0",
},
mockToken: &gocloak.JWT{
AccessToken: "test_token",
},
expectedToken: &gocloak.JWT{
AccessToken: "test_token",
},
expectedError: nil,
shouldFetchToken: true,
},
{
name: "Valid cached token",
cachedToken: &gocloak.JWT{
AccessToken: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjQ4NzMyNDI1ODd9.BHuBKDS9MUC01jmo_p4AcVChkbV0aiDZBXcU-hpj8mg",
},
mockToken: &gocloak.JWT{
AccessToken: "test_token",
},
expectedToken: &gocloak.JWT{
AccessToken: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjQ4NzMyNDI1ODd9.BHuBKDS9MUC01jmo_p4AcVChkbV0aiDZBXcU-hpj8mg",
},
expectedError: nil,
shouldFetchToken: false,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {

mockTokenReceiver := new(MockKeycloakRepository)
cache := NewKeycloakJWTReceiverCachedInMemory(mockTokenReceiver)

cache.cachedToken = tc.cachedToken

mockTokenReceiver.On("getClientToken").Return(tc.mockToken, tc.mockError)

token, err := cache.getClientToken("testClient", "testSecret")

assert.ErrorIs(t, err, tc.expectedError)

assert.Equal(t, tc.expectedToken, token)
if tc.shouldFetchToken {
mockTokenReceiver.AssertCalled(t, "getClientToken")
}
})
}
}

func TestKeycloakJWTReceiverCachedInMemory_GetClientAccessToken(t *testing.T) {
mockKeycloakRepository := new(MockKeycloakRepository)
mockToken := &gocloak.JWT{
AccessToken: "test_token",
ExpiresIn: 3600,
}

mockKeycloakRepository.On("getClientToken").Return(mockToken, nil)

cache := NewKeycloakJWTReceiverCachedInMemory(mockKeycloakRepository)

accessToken, err := cache.GetClientAccessToken("testClient", "testSecret")

assert.NoError(t, err)
assert.Equal(t, "test_token", accessToken)
mockKeycloakRepository.AssertCalled(t, "getClientToken")
}
34 changes: 34 additions & 0 deletions client/keycloakRepository.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// SPDX-FileCopyrightText: 2024 Greenbone AG
//
// SPDX-License-Identifier: GPL-3.0-or-later

package client

import (
"context"

"github.com/Nerzal/gocloak/v12"
)

type IKeycloakRepository interface {
getClientToken(clientName, clientSecret string) (*gocloak.JWT, error)
// Append keycloak functions here
}

type KeycloakRepository struct {
client *gocloak.GoCloak
realm string
}

var _ IKeycloakRepository = &KeycloakRepository{}

func NewKeycloakRepository(basePath, realm string) *KeycloakRepository {
return &KeycloakRepository{
client: gocloak.NewClient(basePath),
realm: realm,
}
}

func (r *KeycloakRepository) getClientToken(clientName, clientSecret string) (*gocloak.JWT, error) {
return r.client.LoginClient(context.Background(), clientName, clientSecret, r.realm)
}
8 changes: 6 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
module github.com/greenbone/keycloak-client-golang

go 1.21
go 1.22

require (
github.com/Nerzal/gocloak/v12 v12.0.0
github.com/gin-gonic/gin v1.10.0
github.com/golang-jwt/jwt/v4 v4.5.0
github.com/golang-jwt/jwt/v5 v5.2.1
github.com/jarcoal/httpmock v1.3.1
github.com/rs/zerolog v1.33.0
github.com/samber/lo v1.39.0
github.com/stretchr/testify v1.9.0
)
Expand All @@ -24,10 +25,12 @@ require (
github.com/go-playground/validator/v10 v10.21.0 // indirect
github.com/go-resty/resty/v2 v2.13.1 // indirect
github.com/goccy/go-json v0.10.3 // indirect
github.com/golang-jwt/jwt/v4 v4.4.2 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
Expand All @@ -36,6 +39,7 @@ require (
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/segmentio/ksuid v1.0.4 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
golang.org/x/arch v0.8.0 // indirect
Expand Down
Loading

0 comments on commit cbf5d5d

Please sign in to comment.