Changes in implementation of Security API - get Token 00/10400/2
authorychacon <yennifer.chacon@est.tech>
Wed, 8 Feb 2023 08:41:34 +0000 (09:41 +0100)
committerychacon <yennifer.chacon@est.tech>
Thu, 9 Feb 2023 14:52:10 +0000 (15:52 +0100)
Issue-ID: NONRTRIC-836
Signed-off-by: ychacon <yennifer.chacon@est.tech>
Change-Id: I5eb350a63582c6e24651dd89f392dab0a9db1d8b

capifcore/internal/securityapi/typeaccess.go [new file with mode: 0644]
capifcore/internal/securityapi/typevalidation.go [new file with mode: 0644]
capifcore/internal/securityapi/typevalidation_test.go [new file with mode: 0644]
capifcore/internal/securityservice/security.go
capifcore/internal/securityservice/security_test.go

diff --git a/capifcore/internal/securityapi/typeaccess.go b/capifcore/internal/securityapi/typeaccess.go
new file mode 100644 (file)
index 0000000..8b88312
--- /dev/null
@@ -0,0 +1,38 @@
+// -
+//   ========================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 securityapi
+
+import (
+       "github.com/labstack/echo/v4"
+)
+
+func (tokenReq *AccessTokenReq) GetAccessTokenReq(ctx echo.Context) {
+       clientId := ctx.FormValue("client_id")
+       clientSecret := ctx.FormValue("client_secret")
+       scope := ctx.FormValue("scope")
+       grantType := ctx.FormValue("grant_type")
+
+       tokenReq.ClientId = clientId
+       tokenReq.ClientSecret = &clientSecret
+       tokenReq.Scope = &scope
+       tokenReq.GrantType = AccessTokenReqGrantType(grantType)
+
+}
diff --git a/capifcore/internal/securityapi/typevalidation.go b/capifcore/internal/securityapi/typevalidation.go
new file mode 100644 (file)
index 0000000..90dbda3
--- /dev/null
@@ -0,0 +1,62 @@
+// -
+//   ========================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 securityapi
+
+import (
+       "strings"
+)
+
+func (tokenReq AccessTokenReq) Validate() (bool, AccessTokenErr) {
+
+       if tokenReq.ClientId == "" {
+               return false, createAccessTokenError(AccessTokenErrErrorInvalidRequest, "Invalid request")
+       }
+
+       if tokenReq.GrantType != AccessTokenReqGrantTypeClientCredentials {
+               return false, createAccessTokenError(AccessTokenErrErrorInvalidGrant, "Invalid value for grant_type")
+       }
+
+       //3gpp#aefId1:apiName1,apiName2,…apiNameX;aefId2:apiName1,apiName2,…apiNameY;…aefIdN:apiName1,apiName2,…apiNameZ
+       if tokenReq.Scope != nil {
+               scope := strings.Split(*tokenReq.Scope, "#")
+               if len(scope) < 2 {
+                       return false, createAccessTokenError(AccessTokenErrErrorInvalidScope, "Malformed scope")
+               }
+               if scope[0] != "3gpp" {
+                       return false, createAccessTokenError(AccessTokenErrErrorInvalidScope, "Scope should start with 3gpp")
+               }
+               aefList := strings.Split(scope[1], ";")
+               for _, aef := range aefList {
+                       apiList := strings.Split(aef, ":")
+                       if len(apiList) < 2 {
+                               return false, createAccessTokenError(AccessTokenErrErrorInvalidScope, "Malformed scope")
+                       }
+               }
+       }
+       return true, AccessTokenErr{}
+}
+
+func createAccessTokenError(err AccessTokenErrError, message string) AccessTokenErr {
+       return AccessTokenErr{
+               Error:            err,
+               ErrorDescription: &message,
+       }
+}
diff --git a/capifcore/internal/securityapi/typevalidation_test.go b/capifcore/internal/securityapi/typevalidation_test.go
new file mode 100644 (file)
index 0000000..0515d06
--- /dev/null
@@ -0,0 +1,97 @@
+// -
+//   ========================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 securityapi
+
+import (
+       "testing"
+
+       "github.com/stretchr/testify/assert"
+)
+
+func TestValidateClientIdNotPresent(t *testing.T) {
+       accessTokenUnderTest := AccessTokenReq{}
+       valid, err := accessTokenUnderTest.Validate()
+
+       assert.Equal(t, false, valid)
+       assert.Equal(t, AccessTokenErrErrorInvalidRequest, err.Error)
+       assert.Equal(t, "Invalid request", *err.ErrorDescription)
+}
+
+func TestValidateGrantType(t *testing.T) {
+       accessTokenUnderTest := AccessTokenReq{
+               ClientId:  "clientId",
+               GrantType: AccessTokenReqGrantType(""),
+       }
+       valid, err := accessTokenUnderTest.Validate()
+
+       assert.Equal(t, false, valid)
+       assert.Equal(t, AccessTokenErrErrorInvalidGrant, err.Error)
+       assert.Equal(t, "Invalid value for grant_type", *err.ErrorDescription)
+
+       accessTokenUnderTest.GrantType = AccessTokenReqGrantType("client_credentials")
+       valid, err = accessTokenUnderTest.Validate()
+       assert.Equal(t, true, valid)
+}
+
+func TestValidateScopeNotValid(t *testing.T) {
+       scope := "scope#aefId:path"
+       accessTokenUnderTest := AccessTokenReq{
+               ClientId:  "clientId",
+               GrantType: ("client_credentials"),
+               Scope:     &scope,
+       }
+       valid, err := accessTokenUnderTest.Validate()
+
+       assert.Equal(t, false, valid)
+       assert.Equal(t, AccessTokenErrErrorInvalidScope, err.Error)
+       assert.Equal(t, "Scope should start with 3gpp", *err.ErrorDescription)
+
+       scope = "3gpp#aefId:path"
+       accessTokenUnderTest.Scope = &scope
+       valid, err = accessTokenUnderTest.Validate()
+       assert.Equal(t, true, valid)
+}
+
+func TestValidateScopeMalformed(t *testing.T) {
+       scope := "3gpp"
+       accessTokenUnderTest := AccessTokenReq{
+               ClientId:  "clientId",
+               GrantType: ("client_credentials"),
+               Scope:     &scope,
+       }
+       valid, err := accessTokenUnderTest.Validate()
+
+       assert.Equal(t, false, valid)
+       assert.Equal(t, AccessTokenErrErrorInvalidScope, err.Error)
+       assert.Equal(t, "Malformed scope", *err.ErrorDescription)
+
+       scope = "3gpp#aefId"
+       accessTokenUnderTest.Scope = &scope
+       valid, err = accessTokenUnderTest.Validate()
+       assert.Equal(t, false, valid)
+       assert.Equal(t, AccessTokenErrErrorInvalidScope, err.Error)
+       assert.Equal(t, "Malformed scope", *err.ErrorDescription)
+
+       scope = "3gpp#aefId:path"
+       accessTokenUnderTest.Scope = &scope
+       valid, err = accessTokenUnderTest.Validate()
+       assert.Equal(t, true, valid)
+}
index 3df2918..d71ff0d 100644 (file)
@@ -23,7 +23,9 @@ package security
 import (
        "net/http"
        "strings"
+       "time"
 
+       "github.com/golang-jwt/jwt"
        "github.com/labstack/echo/v4"
 
        "oransc.org/nonrtric/capifcore/internal/common29122"
@@ -34,6 +36,8 @@ import (
        "oransc.org/nonrtric/capifcore/internal/publishservice"
 )
 
+var jwtKey = "my-secret-key"
+
 type Security struct {
        serviceRegister providermanagement.ServiceRegister
        publishRegister publishservice.PublishRegister
@@ -49,34 +53,62 @@ func NewSecurity(serviceRegister providermanagement.ServiceRegister, publishRegi
 }
 
 func (s *Security) PostSecuritiesSecurityIdToken(ctx echo.Context, securityId string) error {
-       clientId := ctx.FormValue("client_id")
-       clientSecret := ctx.FormValue("client_secret")
-       scope := ctx.FormValue("scope")
+       var accessTokenReq securityapi.AccessTokenReq
+       accessTokenReq.GetAccessTokenReq(ctx)
 
-       if !s.invokerRegister.IsInvokerRegistered(clientId) {
-               return sendCoreError(ctx, http.StatusBadRequest, "Invoker not registered")
+       if valid, err := accessTokenReq.Validate(); !valid {
+               return ctx.JSON(http.StatusBadRequest, err)
        }
-       if !s.invokerRegister.VerifyInvokerSecret(clientId, clientSecret) {
-               return sendCoreError(ctx, http.StatusBadRequest, "Invoker secret not valid")
+
+       if !s.invokerRegister.IsInvokerRegistered(accessTokenReq.ClientId) {
+               return sendAccessTokenError(ctx, http.StatusBadRequest, securityapi.AccessTokenErrErrorInvalidClient, "Invoker not registered")
        }
-       if scope != "" {
-               scopeData := strings.Split(strings.Split(scope, "#")[1], ":")
-               if !s.serviceRegister.IsFunctionRegistered(scopeData[0]) {
-                       return sendCoreError(ctx, http.StatusBadRequest, "Function not registered")
-               }
-               if !s.publishRegister.IsAPIPublished(scopeData[0], scopeData[1]) {
-                       return sendCoreError(ctx, http.StatusBadRequest, "API not published")
+
+       if !s.invokerRegister.VerifyInvokerSecret(accessTokenReq.ClientId, *accessTokenReq.ClientSecret) {
+               return sendAccessTokenError(ctx, http.StatusBadRequest, securityapi.AccessTokenErrErrorUnauthorizedClient, "Invoker secret not valid")
+       }
+
+       if accessTokenReq.Scope != nil {
+               scope := strings.Split(*accessTokenReq.Scope, "#")
+               aefList := strings.Split(scope[1], ";")
+               for _, aef := range aefList {
+                       apiList := strings.Split(aef, ":")
+                       if !s.serviceRegister.IsFunctionRegistered(apiList[0]) {
+                               return sendAccessTokenError(ctx, http.StatusBadRequest, securityapi.AccessTokenErrErrorInvalidScope, "AEF Function not registered")
+                       }
+                       for _, api := range strings.Split(apiList[1], ",") {
+                               if !s.publishRegister.IsAPIPublished(apiList[0], api) {
+                                       return sendAccessTokenError(ctx, http.StatusBadRequest, securityapi.AccessTokenErrErrorInvalidScope, "API not published")
+                               }
+                       }
                }
        }
 
+       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))
+       if err != nil {
+               // If there is an error in creating the JWT return an internal server error
+               return err
+       }
+
        accessTokenResp := securityapi.AccessTokenRsp{
-               AccessToken: "asdadfsrt dsr t5",
-               ExpiresIn:   0,
-               Scope:       &scope,
+               AccessToken: tokenString,
+               ExpiresIn:   common29122.DurationSec(expirationTime),
+               Scope:       accessTokenReq.Scope,
                TokenType:   "Bearer",
        }
 
-       err := ctx.JSON(http.StatusCreated, accessTokenResp)
+       err = ctx.JSON(http.StatusCreated, accessTokenResp)
        if err != nil {
                // Something really bad happened, tell Echo that our handler failed
                return err
@@ -105,11 +137,10 @@ func (s *Security) PostTrustedInvokersApiInvokerIdUpdate(ctx echo.Context, apiIn
        return ctx.NoContent(http.StatusNotImplemented)
 }
 
-func sendCoreError(ctx echo.Context, code int, message string) error {
-       pd := common29122.ProblemDetails{
-               Cause:  &message,
-               Status: &code,
+func sendAccessTokenError(ctx echo.Context, code int, err securityapi.AccessTokenErrError, message string) error {
+       accessTokenErr := securityapi.AccessTokenErr{
+               Error:            err,
+               ErrorDescription: &message,
        }
-       err := ctx.JSON(code, pd)
-       return err
+       return ctx.JSON(code, accessTokenErr)
 }
index 7043cca..6c62161 100644 (file)
@@ -35,8 +35,6 @@ import (
 
        "github.com/labstack/echo/v4"
 
-       "oransc.org/nonrtric/capifcore/internal/common29122"
-
        invokermocks "oransc.org/nonrtric/capifcore/internal/invokermanagement/mocks"
        servicemocks "oransc.org/nonrtric/capifcore/internal/providermanagement/mocks"
        publishmocks "oransc.org/nonrtric/capifcore/internal/publishservice/mocks"
@@ -65,9 +63,10 @@ func TestPostSecurityIdTokenInvokerRegistered(t *testing.T) {
        aefId := "aefId"
        path := "path"
        data.Set("client_id", clientId)
-       data.Add("client_secret", clientSecret)
-       data.Add("grant_type", "client_credentials")
-       data.Add("scope", "scope#"+aefId+":"+path)
+       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)
@@ -77,9 +76,8 @@ func TestPostSecurityIdTokenInvokerRegistered(t *testing.T) {
        err := result.UnmarshalBodyToObject(&resultResponse)
        assert.NoError(t, err, "error unmarshaling response")
        assert.NotEmpty(t, resultResponse.AccessToken)
-       assert.Equal(t, "scope#"+aefId+":"+path, *resultResponse.Scope)
+       assert.Equal(t, "3gpp#"+aefId+":"+path, *resultResponse.Scope)
        assert.Equal(t, securityapi.AccessTokenRspTokenTypeBearer, resultResponse.TokenType)
-       assert.Equal(t, common29122.DurationSec(0), resultResponse.ExpiresIn)
        invokerRegisterMock.AssertCalled(t, "IsInvokerRegistered", clientId)
        invokerRegisterMock.AssertCalled(t, "VerifyInvokerSecret", clientId, clientSecret)
        serviceRegisterMock.AssertCalled(t, "IsFunctionRegistered", aefId)
@@ -96,19 +94,18 @@ func TestPostSecurityIdTokenInvokerNotRegistered(t *testing.T) {
        data.Set("client_id", "id")
        data.Add("client_secret", "secret")
        data.Add("grant_type", "client_credentials")
-       data.Add("scope", "scope#aefId:path")
+       data.Add("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 problemDetails common29122.ProblemDetails
-       err := result.UnmarshalBodyToObject(&problemDetails)
+       var errDetails securityapi.AccessTokenErr
+       err := result.UnmarshalBodyToObject(&errDetails)
        assert.NoError(t, err, "error unmarshaling response")
-       badRequest := http.StatusBadRequest
-       assert.Equal(t, &badRequest, problemDetails.Status)
+       assert.Equal(t, securityapi.AccessTokenErrErrorInvalidClient, errDetails.Error)
        errMsg := "Invoker not registered"
-       assert.Equal(t, &errMsg, problemDetails.Cause)
+       assert.Equal(t, &errMsg, errDetails.ErrorDescription)
 }
 
 func TestPostSecurityIdTokenInvokerSecretNotValid(t *testing.T) {
@@ -122,19 +119,18 @@ func TestPostSecurityIdTokenInvokerSecretNotValid(t *testing.T) {
        data.Set("client_id", "id")
        data.Add("client_secret", "secret")
        data.Add("grant_type", "client_credentials")
-       data.Add("scope", "scope#aefId:path")
+       data.Add("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 problemDetails common29122.ProblemDetails
-       err := result.UnmarshalBodyToObject(&problemDetails)
+       var errDetails securityapi.AccessTokenErr
+       err := result.UnmarshalBodyToObject(&errDetails)
        assert.NoError(t, err, "error unmarshaling response")
-       badRequest := http.StatusBadRequest
-       assert.Equal(t, &badRequest, problemDetails.Status)
+       assert.Equal(t, securityapi.AccessTokenErrErrorUnauthorizedClient, errDetails.Error)
        errMsg := "Invoker secret not valid"
-       assert.Equal(t, &errMsg, problemDetails.Cause)
+       assert.Equal(t, &errMsg, errDetails.ErrorDescription)
 }
 
 func TestPostSecurityIdTokenFunctionNotRegistered(t *testing.T) {
@@ -150,19 +146,18 @@ func TestPostSecurityIdTokenFunctionNotRegistered(t *testing.T) {
        data.Set("client_id", "id")
        data.Add("client_secret", "secret")
        data.Add("grant_type", "client_credentials")
-       data.Add("scope", "scope#aefId:path")
+       data.Add("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 problemDetails common29122.ProblemDetails
-       err := result.UnmarshalBodyToObject(&problemDetails)
+       var errDetails securityapi.AccessTokenErr
+       err := result.UnmarshalBodyToObject(&errDetails)
        assert.NoError(t, err, "error unmarshaling response")
-       badRequest := http.StatusBadRequest
-       assert.Equal(t, &badRequest, problemDetails.Status)
-       errMsg := "Function not registered"
-       assert.Equal(t, &errMsg, problemDetails.Cause)
+       assert.Equal(t, securityapi.AccessTokenErrErrorInvalidScope, errDetails.Error)
+       errMsg := "AEF Function not registered"
+       assert.Equal(t, &errMsg, errDetails.ErrorDescription)
 }
 
 func TestPostSecurityIdTokenAPINotPublished(t *testing.T) {
@@ -180,19 +175,18 @@ func TestPostSecurityIdTokenAPINotPublished(t *testing.T) {
        data.Set("client_id", "id")
        data.Add("client_secret", "secret")
        data.Add("grant_type", "client_credentials")
-       data.Add("scope", "scope#aefId:path")
+       data.Add("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 problemDetails common29122.ProblemDetails
-       err := result.UnmarshalBodyToObject(&problemDetails)
+       var errDetails securityapi.AccessTokenErr
+       err := result.UnmarshalBodyToObject(&errDetails)
        assert.NoError(t, err, "error unmarshaling response")
-       badRequest := http.StatusBadRequest
-       assert.Equal(t, &badRequest, problemDetails.Status)
+       assert.Equal(t, securityapi.AccessTokenErrErrorInvalidScope, errDetails.Error)
        errMsg := "API not published"
-       assert.Equal(t, &errMsg, problemDetails.Cause)
+       assert.Equal(t, &errMsg, errDetails.ErrorDescription)
 }
 
 func getEcho(serviceRegister providermanagement.ServiceRegister, publishRegister publishservice.PublishRegister, invokerRegister invokermanagement.InvokerRegister) *echo.Echo {