Generating token using keycloak 70/10570/2
authorychacon <yennifer.chacon@est.tech>
Thu, 23 Feb 2023 10:13:14 +0000 (11:13 +0100)
committerychacon <yennifer.chacon@est.tech>
Tue, 28 Feb 2023 16:19:13 +0000 (17:19 +0100)
Issue-ID: NONRTRIC-836
Signed-off-by: ychacon <yennifer.chacon@est.tech>
Change-Id: If337576ccac816292709c6d7ab30d239a2d8a77b

capifcore/configs/keycloak.yaml [new file with mode: 0644]
capifcore/internal/config/config.go [new file with mode: 0644]
capifcore/internal/keycloak/keycloak.go [new file with mode: 0644]
capifcore/internal/keycloak/mocks/AccessManagement.go [new file with mode: 0644]
capifcore/internal/securityapi/typevalidation.go
capifcore/internal/securityservice/security.go
capifcore/internal/securityservice/security_test.go
capifcore/main.go

diff --git a/capifcore/configs/keycloak.yaml b/capifcore/configs/keycloak.yaml
new file mode 100644 (file)
index 0000000..1ca2ba9
--- /dev/null
@@ -0,0 +1,23 @@
+#  ============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"
diff --git a/capifcore/internal/config/config.go b/capifcore/internal/config/config.go
new file mode 100644 (file)
index 0000000..4a53022
--- /dev/null
@@ -0,0 +1,55 @@
+// -
+//   ========================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
+}
diff --git a/capifcore/internal/keycloak/keycloak.go b/capifcore/internal/keycloak/keycloak.go
new file mode 100644 (file)
index 0000000..16f65c8
--- /dev/null
@@ -0,0 +1,90 @@
+// -
+//   ========================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
+}
diff --git a/capifcore/internal/keycloak/mocks/AccessManagement.go b/capifcore/internal/keycloak/mocks/AccessManagement.go
new file mode 100644 (file)
index 0000000..9ac2179
--- /dev/null
@@ -0,0 +1,52 @@
+// 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
+}
index 90dbda3..1241f96 100644 (file)
@@ -35,7 +35,7 @@ func (tokenReq AccessTokenReq) Validate() (bool, AccessTokenErr) {
        }
 
        //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")
index d71ff0d..dcf1dbb 100644 (file)
@@ -23,32 +23,31 @@ package security
 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,
        }
 }
 
@@ -68,7 +67,7 @@ func (s *Security) PostSecuritiesSecurityIdToken(ctx echo.Context, securityId st
                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 {
@@ -83,27 +82,14 @@ func (s *Security) PostSecuritiesSecurityIdToken(ctx echo.Context, securityId st
                        }
                }
        }
-
-       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",
        }
index 6c62161..13af737 100644 (file)
 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"
@@ -36,6 +38,7 @@ import (
        "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"
 
@@ -55,7 +58,15 @@ func TestPostSecurityIdTokenInvokerRegistered(t *testing.T) {
        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"
@@ -76,19 +87,19 @@ func TestPostSecurityIdTokenInvokerRegistered(t *testing.T) {
        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")
@@ -113,7 +124,7 @@ func TestPostSecurityIdTokenInvokerSecretNotValid(t *testing.T) {
        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")
@@ -140,7 +151,7 @@ func TestPostSecurityIdTokenFunctionNotRegistered(t *testing.T) {
        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")
@@ -169,7 +180,7 @@ func TestPostSecurityIdTokenAPINotPublished(t *testing.T) {
        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")
@@ -189,7 +200,47 @@ func TestPostSecurityIdTokenAPINotPublished(t *testing.T) {
        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)
@@ -198,7 +249,7 @@ func getEcho(serviceRegister providermanagement.ServiceRegister, publishRegister
 
        swagger.Servers = nil
 
-       s := NewSecurity(serviceRegister, publishRegister, invokerRegister)
+       s := NewSecurity(serviceRegister, publishRegister, invokerRegister, keycloakMgm)
 
        e := echo.New()
        e.Use(echomiddleware.Logger())
index 3ee4192..f8b9daf 100644 (file)
@@ -32,12 +32,14 @@ import (
        "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"
@@ -87,6 +89,13 @@ func getEcho() *echo.Echo {
        // 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()
@@ -150,7 +159,7 @@ func getEcho() *echo.Echo {
                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")