--- /dev/null
+# ============LICENSE_START===============================================
+# Copyright (C) 2023 Nordix Foundation. 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.
+# ============LICENSE_END=================================================
+#
+
+# Keycloak configurations
+authorizationServer:
+ host: "localhost"
+ port: "8080"
+ realms:
+ invokerrealm: "invokerrealm"
--- /dev/null
+// -
+// ========================LICENSE_START=================================
+// O-RAN-SC
+// %%
+// Copyright (C) 2023: Nordix Foundation
+// %%
+// 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.
+// ========================LICENSE_END===================================
+//
+
+package config
+
+import (
+ "os"
+ "path/filepath"
+
+ "gopkg.in/yaml.v2"
+)
+
+type AuthorizationServer struct {
+ Port string `yaml:"port"`
+ Host string `yaml:"host"`
+ Realms map[string]string `yaml:"realms"`
+}
+
+type Config struct {
+ AuthorizationServer AuthorizationServer `yaml:"authorizationServer"`
+}
+
+func ReadKeycloakConfigFile(configFolder string) (*Config, error) {
+
+ f, err := os.Open(filepath.Join(configFolder, "keycloak.yaml"))
+ if err != nil {
+ return nil, err
+ }
+ defer f.Close()
+
+ var cfg *Config
+ decoder := yaml.NewDecoder(f)
+ err = decoder.Decode(&cfg)
+ if err != nil {
+ return nil, err
+ }
+ return cfg, nil
+}
--- /dev/null
+// -
+// ========================LICENSE_START=================================
+// O-RAN-SC
+// %%
+// Copyright (C) 2023: Nordix Foundation
+// %%
+// 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.
+// ========================LICENSE_END===================================
+//
+
+package keycloak
+
+import (
+ "encoding/json"
+ "errors"
+ "io"
+ "net/http"
+ "net/url"
+
+ "oransc.org/nonrtric/capifcore/internal/config"
+)
+
+//go:generate mockery --name AccessManagement
+type AccessManagement interface {
+ // Get JWT token for a client.
+ // Returns JWT token if client exits and credentials are correct otherwise returns error.
+ GetToken(clientId, clientPassword, scope string, realm string) (Jwttoken, error)
+}
+
+type KeycloakManager struct {
+ keycloakServerUrl string
+ realms map[string]string
+}
+
+func NewKeycloakManager(cfg *config.Config) *KeycloakManager {
+
+ keycloakUrl := "http://" + cfg.AuthorizationServer.Host + ":" + cfg.AuthorizationServer.Port
+
+ return &KeycloakManager{
+ keycloakServerUrl: keycloakUrl,
+ realms: cfg.AuthorizationServer.Realms,
+ }
+}
+
+type Jwttoken struct {
+ AccessToken string `json:"access_token"`
+ IDToken string `json:"id_token"`
+ ExpiresIn int `json:"expires_in"`
+ RefreshExpiresIn int `json:"refresh_expires_in"`
+ RefreshToken string `json:"refresh_token"`
+ TokenType string `json:"token_type"`
+ NotBeforePolicy int `json:"not-before-policy"`
+ SessionState string `json:"session_state"`
+ Scope string `json:"scope"`
+}
+
+func (km *KeycloakManager) GetToken(clientId, clientPassword, scope string, realm string) (Jwttoken, error) {
+ var jwt Jwttoken
+ getTokenUrl := km.keycloakServerUrl + "/realms/" + realm + "/protocol/openid-connect/token"
+
+ resp, err := http.PostForm(getTokenUrl,
+ url.Values{"grant_type": {"client_credentials"}, "client_id": {clientId}, "client_secret": {clientPassword}})
+
+ if err != nil {
+ return jwt, err
+ }
+
+ defer resp.Body.Close()
+ body, err := io.ReadAll(resp.Body)
+
+ if err != nil {
+ return jwt, err
+ }
+ if resp.StatusCode != http.StatusOK {
+ return jwt, errors.New(string(body))
+ }
+
+ json.Unmarshal([]byte(body), &jwt)
+ return jwt, nil
+}
--- /dev/null
+// Code generated by mockery v2.20.0. DO NOT EDIT.
+
+package mocks
+
+import (
+ mock "github.com/stretchr/testify/mock"
+ keycloak "oransc.org/nonrtric/capifcore/internal/keycloak"
+)
+
+// AccessManagement is an autogenerated mock type for the AccessManagement type
+type AccessManagement struct {
+ mock.Mock
+}
+
+// GetToken provides a mock function with given fields: clientId, clientPassword, scope, realm
+func (_m *AccessManagement) GetToken(clientId string, clientPassword string, scope string, realm string) (keycloak.Jwttoken, error) {
+ ret := _m.Called(clientId, clientPassword, scope, realm)
+
+ var r0 keycloak.Jwttoken
+ var r1 error
+ if rf, ok := ret.Get(0).(func(string, string, string, string) (keycloak.Jwttoken, error)); ok {
+ return rf(clientId, clientPassword, scope, realm)
+ }
+ if rf, ok := ret.Get(0).(func(string, string, string, string) keycloak.Jwttoken); ok {
+ r0 = rf(clientId, clientPassword, scope, realm)
+ } else {
+ r0 = ret.Get(0).(keycloak.Jwttoken)
+ }
+
+ if rf, ok := ret.Get(1).(func(string, string, string, string) error); ok {
+ r1 = rf(clientId, clientPassword, scope, realm)
+ } else {
+ r1 = ret.Error(1)
+ }
+
+ return r0, r1
+}
+
+type mockConstructorTestingTNewAccessManagement interface {
+ mock.TestingT
+ Cleanup(func())
+}
+
+// NewAccessManagement creates a new instance of AccessManagement. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
+func NewAccessManagement(t mockConstructorTestingTNewAccessManagement) *AccessManagement {
+ mock := &AccessManagement{}
+ mock.Mock.Test(t)
+
+ t.Cleanup(func() { mock.AssertExpectations(t) })
+
+ return mock
+}
}
//3gpp#aefId1:apiName1,apiName2,…apiNameX;aefId2:apiName1,apiName2,…apiNameY;…aefIdN:apiName1,apiName2,…apiNameZ
- if tokenReq.Scope != nil {
+ if tokenReq.Scope != nil && *tokenReq.Scope != "" {
scope := strings.Split(*tokenReq.Scope, "#")
if len(scope) < 2 {
return false, createAccessTokenError(AccessTokenErrErrorInvalidScope, "Malformed scope")
import (
"net/http"
"strings"
- "time"
- "github.com/golang-jwt/jwt"
"github.com/labstack/echo/v4"
"oransc.org/nonrtric/capifcore/internal/common29122"
securityapi "oransc.org/nonrtric/capifcore/internal/securityapi"
"oransc.org/nonrtric/capifcore/internal/invokermanagement"
+ "oransc.org/nonrtric/capifcore/internal/keycloak"
"oransc.org/nonrtric/capifcore/internal/providermanagement"
"oransc.org/nonrtric/capifcore/internal/publishservice"
)
-var jwtKey = "my-secret-key"
-
type Security struct {
serviceRegister providermanagement.ServiceRegister
publishRegister publishservice.PublishRegister
invokerRegister invokermanagement.InvokerRegister
+ keycloak keycloak.AccessManagement
}
-func NewSecurity(serviceRegister providermanagement.ServiceRegister, publishRegister publishservice.PublishRegister, invokerRegister invokermanagement.InvokerRegister) *Security {
+func NewSecurity(serviceRegister providermanagement.ServiceRegister, publishRegister publishservice.PublishRegister, invokerRegister invokermanagement.InvokerRegister, km keycloak.AccessManagement) *Security {
return &Security{
serviceRegister: serviceRegister,
publishRegister: publishRegister,
invokerRegister: invokerRegister,
+ keycloak: km,
}
}
return sendAccessTokenError(ctx, http.StatusBadRequest, securityapi.AccessTokenErrErrorUnauthorizedClient, "Invoker secret not valid")
}
- if accessTokenReq.Scope != nil {
+ if accessTokenReq.Scope != nil && *accessTokenReq.Scope != "" {
scope := strings.Split(*accessTokenReq.Scope, "#")
aefList := strings.Split(scope[1], ";")
for _, aef := range aefList {
}
}
}
-
- expirationTime := time.Now().Add(time.Hour).Unix()
-
- claims := &jwt.MapClaims{
- "iss": accessTokenReq.ClientId,
- "exp": expirationTime,
- "data": map[string]interface{}{
- "scope": accessTokenReq.Scope,
- },
- }
-
- token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
- tokenString, err := token.SignedString([]byte(jwtKey))
+ jwtToken, err := s.keycloak.GetToken(accessTokenReq.ClientId, *accessTokenReq.ClientSecret, *accessTokenReq.Scope, "invokerrealm")
if err != nil {
- // If there is an error in creating the JWT return an internal server error
- return err
+ return sendAccessTokenError(ctx, http.StatusBadRequest, securityapi.AccessTokenErrErrorUnauthorizedClient, err.Error())
}
accessTokenResp := securityapi.AccessTokenRsp{
- AccessToken: tokenString,
- ExpiresIn: common29122.DurationSec(expirationTime),
+ AccessToken: jwtToken.AccessToken,
+ ExpiresIn: common29122.DurationSec(jwtToken.ExpiresIn),
Scope: accessTokenReq.Scope,
TokenType: "Bearer",
}
package security
import (
+ "errors"
"fmt"
"net/http"
"net/url"
"os"
"testing"
+ "oransc.org/nonrtric/capifcore/internal/keycloak"
"oransc.org/nonrtric/capifcore/internal/securityapi"
"oransc.org/nonrtric/capifcore/internal/invokermanagement"
"github.com/labstack/echo/v4"
invokermocks "oransc.org/nonrtric/capifcore/internal/invokermanagement/mocks"
+ keycloackmocks "oransc.org/nonrtric/capifcore/internal/keycloak/mocks"
servicemocks "oransc.org/nonrtric/capifcore/internal/providermanagement/mocks"
publishmocks "oransc.org/nonrtric/capifcore/internal/publishservice/mocks"
publishRegisterMock := publishmocks.PublishRegister{}
publishRegisterMock.On("IsAPIPublished", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(true)
- requestHandler := getEcho(&serviceRegisterMock, &publishRegisterMock, &invokerRegisterMock)
+ jwt := keycloak.Jwttoken{
+ AccessToken: "eyJhbGNIn0.e3YTQ0xLjEifQ.FcqCwCy7iJiOmw",
+ ExpiresIn: 300,
+ Scope: "3gpp#aefIdpath",
+ }
+ accessMgmMock := keycloackmocks.AccessManagement{}
+ accessMgmMock.On("GetToken", mock.AnythingOfType("string"), mock.AnythingOfType("string"), mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(jwt, nil)
+
+ requestHandler := getEcho(&serviceRegisterMock, &publishRegisterMock, &invokerRegisterMock, &accessMgmMock)
data := url.Values{}
clientId := "id"
err := result.UnmarshalBodyToObject(&resultResponse)
assert.NoError(t, err, "error unmarshaling response")
assert.NotEmpty(t, resultResponse.AccessToken)
- assert.Equal(t, "3gpp#"+aefId+":"+path, *resultResponse.Scope)
assert.Equal(t, securityapi.AccessTokenRspTokenTypeBearer, resultResponse.TokenType)
invokerRegisterMock.AssertCalled(t, "IsInvokerRegistered", clientId)
invokerRegisterMock.AssertCalled(t, "VerifyInvokerSecret", clientId, clientSecret)
serviceRegisterMock.AssertCalled(t, "IsFunctionRegistered", aefId)
publishRegisterMock.AssertCalled(t, "IsAPIPublished", aefId, path)
+ accessMgmMock.AssertCalled(t, "GetToken", clientId, clientSecret, "3gpp#"+aefId+":"+path, "invokerrealm")
}
func TestPostSecurityIdTokenInvokerNotRegistered(t *testing.T) {
invokerRegisterMock := invokermocks.InvokerRegister{}
invokerRegisterMock.On("IsInvokerRegistered", mock.AnythingOfType("string")).Return(false)
- requestHandler := getEcho(nil, nil, &invokerRegisterMock)
+ requestHandler := getEcho(nil, nil, &invokerRegisterMock, nil)
data := url.Values{}
data.Set("client_id", "id")
invokerRegisterMock.On("IsInvokerRegistered", mock.AnythingOfType("string")).Return(true)
invokerRegisterMock.On("VerifyInvokerSecret", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(false)
- requestHandler := getEcho(nil, nil, &invokerRegisterMock)
+ requestHandler := getEcho(nil, nil, &invokerRegisterMock, nil)
data := url.Values{}
data.Set("client_id", "id")
serviceRegisterMock := servicemocks.ServiceRegister{}
serviceRegisterMock.On("IsFunctionRegistered", mock.AnythingOfType("string")).Return(false)
- requestHandler := getEcho(&serviceRegisterMock, nil, &invokerRegisterMock)
+ requestHandler := getEcho(&serviceRegisterMock, nil, &invokerRegisterMock, nil)
data := url.Values{}
data.Set("client_id", "id")
publishRegisterMock := publishmocks.PublishRegister{}
publishRegisterMock.On("IsAPIPublished", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(false)
- requestHandler := getEcho(&serviceRegisterMock, &publishRegisterMock, &invokerRegisterMock)
+ requestHandler := getEcho(&serviceRegisterMock, &publishRegisterMock, &invokerRegisterMock, nil)
data := url.Values{}
data.Set("client_id", "id")
assert.Equal(t, &errMsg, errDetails.ErrorDescription)
}
-func getEcho(serviceRegister providermanagement.ServiceRegister, publishRegister publishservice.PublishRegister, invokerRegister invokermanagement.InvokerRegister) *echo.Echo {
+func TestPostSecurityIdTokenInvokerInvalidCredentials(t *testing.T) {
+ invokerRegisterMock := invokermocks.InvokerRegister{}
+ invokerRegisterMock.On("IsInvokerRegistered", mock.AnythingOfType("string")).Return(true)
+ invokerRegisterMock.On("VerifyInvokerSecret", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(true)
+ serviceRegisterMock := servicemocks.ServiceRegister{}
+ serviceRegisterMock.On("IsFunctionRegistered", mock.AnythingOfType("string")).Return(true)
+ publishRegisterMock := publishmocks.PublishRegister{}
+ publishRegisterMock.On("IsAPIPublished", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(true)
+
+ jwt := keycloak.Jwttoken{}
+ accessMgmMock := keycloackmocks.AccessManagement{}
+ accessMgmMock.On("GetToken", mock.AnythingOfType("string"), mock.AnythingOfType("string"), mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(jwt, errors.New("invalid_credentials"))
+
+ requestHandler := getEcho(&serviceRegisterMock, &publishRegisterMock, &invokerRegisterMock, &accessMgmMock)
+
+ data := url.Values{}
+ clientId := "id"
+ clientSecret := "secret"
+ aefId := "aefId"
+ path := "path"
+ data.Set("client_id", clientId)
+ data.Set("client_secret", clientSecret)
+ data.Set("grant_type", "client_credentials")
+ data.Set("scope", "3gpp#"+aefId+":"+path)
+
+ encodedData := data.Encode()
+
+ result := testutil.NewRequest().Post("/securities/invokerId/token").WithContentType("application/x-www-form-urlencoded").WithBody([]byte(encodedData)).Go(t, requestHandler)
+
+ assert.Equal(t, http.StatusBadRequest, result.Code())
+ var resultResponse securityapi.AccessTokenErr
+ err := result.UnmarshalBodyToObject(&resultResponse)
+ assert.NoError(t, err, "error unmarshaling response")
+ invokerRegisterMock.AssertCalled(t, "IsInvokerRegistered", clientId)
+ invokerRegisterMock.AssertCalled(t, "VerifyInvokerSecret", clientId, clientSecret)
+ serviceRegisterMock.AssertCalled(t, "IsFunctionRegistered", aefId)
+ publishRegisterMock.AssertCalled(t, "IsAPIPublished", aefId, path)
+ accessMgmMock.AssertCalled(t, "GetToken", clientId, clientSecret, "3gpp#"+aefId+":"+path, "invokerrealm")
+}
+
+func getEcho(serviceRegister providermanagement.ServiceRegister, publishRegister publishservice.PublishRegister, invokerRegister invokermanagement.InvokerRegister, keycloakMgm keycloak.AccessManagement) *echo.Echo {
swagger, err := securityapi.GetSwagger()
if err != nil {
fmt.Fprintf(os.Stderr, "Error loading swagger spec\n: %s", err)
swagger.Servers = nil
- s := NewSecurity(serviceRegister, publishRegister, invokerRegister)
+ s := NewSecurity(serviceRegister, publishRegister, invokerRegister, keycloakMgm)
e := echo.New()
e.Use(echomiddleware.Logger())
"oransc.org/nonrtric/capifcore/internal/discoverserviceapi"
"oransc.org/nonrtric/capifcore/internal/eventsapi"
"oransc.org/nonrtric/capifcore/internal/invokermanagementapi"
+ "oransc.org/nonrtric/capifcore/internal/keycloak"
"oransc.org/nonrtric/capifcore/internal/providermanagementapi"
"oransc.org/nonrtric/capifcore/internal/securityapi"
"github.com/deepmap/oapi-codegen/pkg/middleware"
echomiddleware "github.com/labstack/echo/v4/middleware"
log "github.com/sirupsen/logrus"
+ config "oransc.org/nonrtric/capifcore/internal/config"
"oransc.org/nonrtric/capifcore/internal/discoverservice"
"oransc.org/nonrtric/capifcore/internal/eventservice"
"oransc.org/nonrtric/capifcore/internal/helmmanagement"
// Log all requests
e.Use(echomiddleware.Logger())
+ // Read configuration file
+ cfg, err := config.ReadKeycloakConfigFile("configs")
+ if err != nil {
+ log.Fatalf("Error loading configuration file\n: %s", err)
+ }
+ km := keycloak.NewKeycloakManager(cfg)
+
var group *echo.Group
// Register ProviderManagement
providerManagerSwagger, err := providermanagementapi.GetSwagger()
log.Fatalf("Error loading Security swagger spec\n: %s", err)
}
securitySwagger.Servers = nil
- securityService := security.NewSecurity(providerManager, publishService, invokerManager)
+ securityService := security.NewSecurity(providerManager, publishService, invokerManager, km)
group = e.Group("/capif-security/v1")
group.Use(middleware.OapiRequestValidator(securitySwagger))
securityapi.RegisterHandlersWithBaseURL(e, securityService, "/capif-security/v1")