Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions oauth/get_token_context.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ type cachedToken struct {
var tokenMap sync.Map
var clientMap sync.Map

type tokenCacheKey struct {
NFType models.NrfNfManagementNfType
TargetNF models.NrfNfManagementNfType
NFID string
NRFURI string
Scope string
}

func GetTokenCtx(
nfType, targetNF models.NrfNfManagementNfType,
nfId, nrfUri, scope string,
Expand Down Expand Up @@ -49,7 +57,10 @@ func sendAccTokenReq(
}

// Check if we have a valid cached token
if val, ok := tokenMap.Load(scope); ok {
cacheKey := tokenCacheKey{
NFType: nfType, TargetNF: targetNF, NFID: nfId, NRFURI: nrfUri, Scope: scope,
}
if val, ok := tokenMap.Load(cacheKey); ok {
cached := val.(cachedToken)
// Compare current time with absolute expiry timestamp
if time.Now().Unix() < cached.ExpiryTime {
Expand Down Expand Up @@ -79,7 +90,7 @@ func sendAccTokenReq(
Response: res.NrfAccessTokenAccessTokenRsp,
ExpiryTime: expiryTime,
}
tokenMap.Store(scope, cached)
tokenMap.Store(cacheKey, cached)

token := &oauth2.Token{
AccessToken: res.NrfAccessTokenAccessTokenRsp.AccessToken,
Expand All @@ -91,3 +102,4 @@ func sendAccTokenReq(
return nil, nil, openapi.ReportError("server no response")
}
}

18 changes: 13 additions & 5 deletions oauth/oauth.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@ import (

"github.com/golang-jwt/jwt/v5"
"github.com/pkg/errors"

"github.com/free5gc/openapi/models"
)

type CCAClaims struct {
Expand All @@ -26,6 +24,15 @@ type CCAClaims struct {
jwt.RegisteredClaims
}

// accessTokenClaims deliberately uses only jwt.RegisteredClaims for the
// standard JWT fields. The generated free5GC model exposes duplicate iss,
// sub, aud, and exp fields alongside RegisteredClaims; decoding into that
// model leaves the validator's embedded fields empty.
type accessTokenClaims struct {
Scope string `json:"scope,omitempty"`
jwt.RegisteredClaims
}

func GenerateClientCredentialAssertion(
sub, aud, keyPath string,
) (string, error) {
Expand Down Expand Up @@ -63,14 +70,14 @@ func VerifyOAuth(
}

auth_fields := strings.Fields(authorization)
if len(auth_fields) < 2 {
if len(auth_fields) != 2 || !strings.EqualFold(auth_fields[0], "Bearer") {
return errors.Errorf("verify OAuth Authorization header invalid")
}

access_token := auth_fields[1]
token, err := jwt.ParseWithClaims(
access_token,
&models.NrfAccessTokenAccessTokenClaims{},
&accessTokenClaims{},
func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
return nil, errors.Wrapf(err, "Unexpected signing method")
Expand All @@ -84,7 +91,7 @@ func VerifyOAuth(
return errors.Wrapf(err, "verify OAuth parse")
}

if !verifyScope(token.Claims.(*models.NrfAccessTokenAccessTokenClaims).Scope, serviceName) {
if !verifyScope(token.Claims.(*accessTokenClaims).Scope, serviceName) {
return errors.New("OAuth scope verification failed: insufficient permissions")
}
return nil
Expand Down Expand Up @@ -304,3 +311,4 @@ func GetNFCertPath(base, nfType, nfId string) string {
// Note: NF's cert should be put in the same base path
return filepath.Join(base, GetNFCertFileName(nfType, nfId))
}

55 changes: 55 additions & 0 deletions oauth/oauth_hardening_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package oauth

import (
"testing"
"time"

"github.com/free5gc/openapi/models"
"github.com/golang-jwt/jwt/v5"
)

func TestVerifyOAuthRejectsExpiredGeneratedModelToken(t *testing.T) {
dir := t.TempDir()
pubPath, privPath := dir+"/public.pem", dir+"/private.pem"
key, err := GenerateRSAKeyPair(pubPath, privPath)
if err != nil {
t.Fatal(err)
}
claims := models.AccessTokenClaims{
Scope: "svc",
Exp: int32(time.Now().Add(-time.Minute).Unix()),
RegisteredClaims: jwt.RegisteredClaims{
Issuer: "issuer",
},
}
tok := jwt.NewWithClaims(jwt.SigningMethodRS512, claims)
signed, err := tok.SignedString(key)
if err != nil {
t.Fatal(err)
}
if err := VerifyOAuth("Bearer "+signed, "svc", pubPath); err == nil {
t.Fatal("expired token was accepted")
}
}

func TestVerifyOAuthRequiresBearerScheme(t *testing.T) {
dir := t.TempDir()
pubPath, privPath := dir+"/public.pem", dir+"/private.pem"
key, err := GenerateRSAKeyPair(pubPath, privPath)
if err != nil {
t.Fatal(err)
}
claims := models.AccessTokenClaims{
Scope: "svc",
Exp: int32(time.Now().Add(time.Minute).Unix()),
}
tok := jwt.NewWithClaims(jwt.SigningMethodRS512, claims)
signed, err := tok.SignedString(key)
if err != nil {
t.Fatal(err)
}
if err := VerifyOAuth("Basic "+signed, "svc", pubPath); err == nil {
t.Fatal("non-Bearer authorization scheme was accepted")
}
}