Skip to content

Commit

Permalink
Merge pull request #60 from synadia-io/op-acc-sign
Browse files Browse the repository at this point in the history
Add `IssueClaim` method for operators and accounts
  • Loading branch information
aricart authored Jan 25, 2025
2 parents f3fcd62 + ed02f87 commit db5f876
Show file tree
Hide file tree
Showing 5 changed files with 181 additions and 3 deletions.
36 changes: 33 additions & 3 deletions accounts.go
Original file line number Diff line number Diff line change
Expand Up @@ -180,15 +180,45 @@ func (a *AccountData) ExternalAuthorization() ([]string, []string, string) {
}

func (a *AccountData) IssueAuthorizationResponse(claim *jwt.AuthorizationResponseClaims, key string) (string, error) {
return a.IssueClaim(claim, key)
}

func (a *AccountData) IssueClaim(claim jwt.Claims, key string) (string, error) {
if key == "" {
key = a.Key.Public
}
k, signingKey, err := a.getKey(key)
k, _, err := a.getKey(key)
if err != nil {
return "", err
}
if signingKey {
claim.IssuerAccount = a.Key.Public
_, scoped := a.ScopedSigningKeys().Contains(key)

switch c := claim.(type) {
case *jwt.OperatorClaims:
return "", errors.New("accounts cannot issue operator claims")
case *jwt.AccountClaims:
if c.Subject != k.Public {
return "", errors.New("accounts can only self-sign")
}
case *jwt.UserClaims:
if scoped {
// cannot have any sort of permission
c.UserPermissionLimits = jwt.UserPermissionLimits{}
}
c.IssuerAccount = a.Key.Public
case *jwt.AuthorizationResponseClaims:
if scoped {
return "", fmt.Errorf("scoped keys can only issue user claims")
}
if key != a.Key.Public {
c.IssuerAccount = a.Key.Public
}
case *jwt.AuthorizationRequestClaims:
return "", errors.New("accounts cannot issue authorization request claims")
case *jwt.GenericClaims:
if scoped {
return "", fmt.Errorf("scoped keys can only issue user claims")
}
}
return a.Operator.SigningService.Sign(claim, k)
}
Expand Down
27 changes: 27 additions & 0 deletions operator.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,33 @@ func (o *OperatorData) update() error {
return nil
}

func (o *OperatorData) IssueClaim(claim jwt.Claims, key string) (string, error) {
switch claim.(type) {
case *jwt.UserClaims:
return "", errors.New("operators cannot issue user claims")
case *jwt.AuthorizationResponseClaims:
return "", errors.New("operators cannot issue authorization response claims")
case *jwt.AuthorizationRequestClaims:
return "", errors.New("operators cannot issue authorization request claims")
}

var k *Key
if key == "" {
k = o.Key
} else {
for _, sk := range o.OperatorSigningKeys {
if sk.Public == key {
k = sk
break
}
}
}
if k == nil {
return "", fmt.Errorf("invalid signing key %w", ErrNotFound)
}
return o.SigningService.Sign(claim, k)
}

func (o *OperatorData) MemResolver() ([]byte, error) {
builder := NewMemResolverConfigBuilder()
if err := builder.Add([]byte(o.Token)); err != nil {
Expand Down
78 changes: 78 additions & 0 deletions tests/accounts_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -951,3 +951,81 @@ func (t *ProviderSuite) Test_ExternalAuthorization() {
t.Nil(accounts)
t.Empty(key)
}

func (t *ProviderSuite) Test_AccountSignClaim() {
auth, err := authb.NewAuth(t.Provider)
t.NoError(err)
o, err := auth.Operators().Add("O")
t.NoError(err)
a, err := o.Accounts().Add("A")
t.NoError(err)
sk, err := a.ScopedSigningKeys().Add()
t.NoError(err)
scope, err := a.ScopedSigningKeys().AddScope("sentinel")
t.NoError(err)
t.NoError(scope.SubPermissions().SetDeny(">"))
t.NoError(scope.PubPermissions().SetDeny(">"))

gc := jwt.NewGenericClaims(a.Subject())
gc.Name = "t"
gc.Data["testing"] = "foo"
token, err := a.IssueClaim(gc, "")
t.NoError(err)
gc, err = jwt.DecodeGeneric(token)
t.NoError(err)
t.Equal(gc.Issuer, a.Subject())
_, err = a.IssueClaim(gc, scope.Key())
t.Error(err)
t.Contains(err.Error(), "scoped keys can only issue user claims")

ukey, err := auth.NewKey(nkeys.PrefixByteUser)
t.NoError(err)
token, err = a.IssueClaim(jwt.NewUserClaims(ukey.Public), "")
t.NoError(err)
u, err := jwt.DecodeUserClaims(token)
t.NoError(err)
t.Equal(u.Issuer, a.Subject())

// signing key keeps perms
uc := jwt.NewUserClaims(ukey.Public)
uc.UserPermissionLimits = jwt.UserPermissionLimits{}
uc.UserPermissionLimits.Permissions.Pub.Allow.Add("foo")
token, err = a.IssueClaim(uc, sk)
t.NoError(err)
u, err = jwt.DecodeUserClaims(token)
t.NoError(err)
t.Equal(u.Issuer, sk)
t.Equal(u.IssuerAccount, a.Subject())
t.True(u.UserPermissionLimits.Permissions.Pub.Allow.Contains("foo"))

// scoped deletes pub perms
token, err = a.IssueClaim(jwt.NewUserClaims(ukey.Public), scope.Key())
t.NoError(err)
u, err = jwt.DecodeUserClaims(token)
t.NoError(err)
t.False(u.UserPermissionLimits.Permissions.Pub.Allow.Contains("foo"))

ar := jwt.NewAuthorizationRequestClaims(ukey.Public)
_, err = a.IssueClaim(ar, "")
t.Error(err)
t.Contains(err.Error(), "accounts cannot issue authorization")

_, err = a.IssueClaim(jwt.NewGenericClaims(ukey.Public), "")
t.NoError(err)

_, err = a.IssueClaim(jwt.NewGenericClaims(ukey.Public), scope.Key())
t.Error(err)
t.Contains(err.Error(), "scoped keys can only issue user claims")

_, err = a.IssueClaim(jwt.NewOperatorClaims(o.Subject()), "")
t.Error(err)
t.Contains(err.Error(), "accounts cannot issue operator claims")

ac, err := jwt.DecodeAccountClaims(a.JWT())
t.NoError(err)
_, err = a.IssueClaim(ac, "")
t.NoError(err)
_, err = a.IssueClaim(ac, scope.Key())
t.Error(err)
t.Contains(err.Error(), "accounts can only self-sign")
}
38 changes: 38 additions & 0 deletions tests/operator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,44 @@ func (t *ProviderSuite) Test_OperatorSystemAccount() {
t.NoError(o.Accounts().Delete("SYS"))
}

func (t *ProviderSuite) Test_OperatorSignClaim() {
auth, err := authb.NewAuth(t.Provider)
t.NoError(err)
o, err := auth.Operators().Add("O")
t.NoError(err)
sk, err := o.SigningKeys().Add()
t.NoError(err)

gc := jwt.NewGenericClaims(o.Subject())
gc.Name = "t"
gc.Data["testing"] = "foo"
token, err := o.IssueClaim(gc, "")
t.NoError(err)
gc, err = jwt.DecodeGeneric(token)
t.NoError(err)
t.Equal(gc.Issuer, o.Subject())

token, err = o.IssueClaim(gc, sk)
t.NoError(err)
gc, err = jwt.DecodeGeneric(token)
t.NoError(err)
t.Equal(gc.Issuer, sk)

k, err := auth.NewKey(nkeys.PrefixByteUser)
t.NoError(err)
_, err = o.IssueClaim(jwt.NewUserClaims(k.Public), "")
t.Error(err)
t.Contains(err.Error(), "operators cannot issue")

_, err = o.IssueClaim(jwt.NewAuthorizationRequestClaims(k.Public), "")
t.Error(err)
t.Contains(err.Error(), "operators cannot issue")

_, err = o.IssueClaim(jwt.NewAuthorizationResponseClaims(k.Public), "")
t.Error(err)
t.Contains(err.Error(), "operators cannot issue")
}

func (t *ProviderSuite) Test_MemResolver() {
auth, err := authb.NewAuth(t.Provider)
t.NoError(err)
Expand Down
5 changes: 5 additions & 0 deletions types.go
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,8 @@ type Operator interface {
JWT() string
// Tags returns an object that you can use to manage tags for the operator
Tags() Tags
// IssueClaim issues the specified jwt.Claim using the specified operator key
IssueClaim(claim jwt.Claims, key string) (string, error)
}

// Accounts is an interface for managing accounts
Expand Down Expand Up @@ -332,7 +334,10 @@ type Account interface {
// if the users value is nil, ExternalAuthorization is not enabled
ExternalAuthorization() ([]string, []string, string)

// IssueAuthorizationResponse generates a signed JWT token for an AuthorizationResponseClaims using the specified key.
IssueAuthorizationResponse(claim *jwt.AuthorizationResponseClaims, key string) (string, error)
// IssueClaim issues the specified jwt.Claim using the specified account key
IssueClaim(claim jwt.Claims, key string) (string, error)
}

// Users is an interface for managing users
Expand Down

0 comments on commit db5f876

Please sign in to comment.