From: ychacon Date: Wed, 8 Feb 2023 08:41:34 +0000 (+0100) Subject: Changes in implementation of Security API - get Token X-Git-Tag: 1.1.0~21^2 X-Git-Url: https://gerrit.o-ran-sc.org/r/gitweb?a=commitdiff_plain;h=cfa08775db2ed44e603b0ceccf36a50f59bd679a;p=nonrtric%2Fplt%2Fsme.git Changes in implementation of Security API - get Token Issue-ID: NONRTRIC-836 Signed-off-by: ychacon Change-Id: I5eb350a63582c6e24651dd89f392dab0a9db1d8b --- diff --git a/capifcore/internal/securityapi/typeaccess.go b/capifcore/internal/securityapi/typeaccess.go new file mode 100644 index 0000000..8b88312 --- /dev/null +++ b/capifcore/internal/securityapi/typeaccess.go @@ -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 index 0000000..90dbda3 --- /dev/null +++ b/capifcore/internal/securityapi/typevalidation.go @@ -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 index 0000000..0515d06 --- /dev/null +++ b/capifcore/internal/securityapi/typevalidation_test.go @@ -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) +} diff --git a/capifcore/internal/securityservice/security.go b/capifcore/internal/securityservice/security.go index 3df2918..d71ff0d 100644 --- a/capifcore/internal/securityservice/security.go +++ b/capifcore/internal/securityservice/security.go @@ -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) } diff --git a/capifcore/internal/securityservice/security_test.go b/capifcore/internal/securityservice/security_test.go index 7043cca..6c62161 100644 --- a/capifcore/internal/securityservice/security_test.go +++ b/capifcore/internal/securityservice/security_test.go @@ -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 {