Skip to content

Commit

Permalink
implement token claims role parsing to string slice not to any slice
Browse files Browse the repository at this point in the history
  • Loading branch information
erudenko committed Jul 14, 2023
1 parent cc482ce commit bf55979
Show file tree
Hide file tree
Showing 4 changed files with 191 additions and 138 deletions.
41 changes: 36 additions & 5 deletions model/token.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ const (
TokenTypeActor TokenType = "actor" // actor token is token impersonation. Admin could impersonated to be some of the users.
)

func (t TokenType) String() string {
return string(t)
}

// TokenWithClaims generates new JWT token with claims and keyID.
func TokenWithClaims(method jwt.SigningMethod, kid string, claims jwt.Claims) *JWToken {
return &JWToken{
Expand Down Expand Up @@ -87,7 +91,7 @@ func (t *JWToken) Payload() map[string]any {

// Type returns token type.
func (t *JWToken) Type() TokenType {
claims, ok := t.Claims.(*Claims)
claims, ok := t.Claims.(Claims)
if !ok {
return ""
}
Expand Down Expand Up @@ -180,8 +184,12 @@ func (c Claims) MarshalJSON() ([]byte, error) {
return nil, err
}
maps.Copy(m, rcm)
m["type"] = c.Type
m["kid"] = c.KeyID
if len(c.Type) > 0 {
m["type"] = c.Type
}
if len(c.KeyID) > 0 {
m["kid"] = c.KeyID
}
return json.Marshal(&m)
}

Expand All @@ -200,8 +208,14 @@ func (c *Claims) UnmarshalJSON(data []byte) error {
exclude := map[string]bool{"iss": true, "sub": true, "aud": true, "exp": true, "nbf": true, "iat": true, "jti": true, "kid": true, "type": true}
c.Payload = map[string]any{}
for k, v := range pc {
if !exclude[strings.ToLower(k)] {
c.Payload[strings.ToLower(k)] = v
lk := strings.ToLower(k)
if !exclude[lk] {
// try to convert slice of any to slice of strings for roles
if strings.HasPrefix(lk, RoleScopePrefix) {
c.Payload[lk] = toSliceString(v)
} else {
c.Payload[lk] = v
}
}
}
if pc["kid"] != nil {
Expand All @@ -214,5 +228,22 @@ func (c *Claims) UnmarshalJSON(data []byte) error {
return nil
}

// if it fail on any conversion - return the original value
func toSliceString(s any) any {
sl, ok := s.([]any)
if !ok {
return s
}
r := []string{}
for _, v := range sl {
vs, ok := v.(string)
if !ok {
return s
}
r = append(r, vs)
}
return r
}

// Full example of how to use JWT tokens:
// https://github.com/form3tech-oss/jwt-go/blob/master/cmd/jwt/app.go
25 changes: 23 additions & 2 deletions model/token_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,18 +34,39 @@ func TestClaimsMarshalJSON(t *testing.T) {
assert.Equal(t, "subject", rcm["sub"])
assert.Equal(t, "value1", rcm["key1"])
assert.Equal(t, "value2", rcm["key2"])

cl.Type = model.TokenTypeActor.String()
cl.ID = "12345"
cl.KeyID = "12345"
b, err = json.Marshal(cl)
require.NoError(t, err)

err = json.Unmarshal(b, &rcm)
require.NoError(t, err)
assert.Len(t, rcm, 7)
assert.Equal(t, "12345", rcm["kid"])
assert.Equal(t, "12345", rcm["jti"])
assert.Equal(t, model.TokenTypeActor.String(), rcm["type"])
}

func TestClaimsUnmarshalJSON(t *testing.T) {
data := `{"iss":"issuer","key1":"value1","key2":"value2","sub":"subject"}`
data := `{"iss":"issuer","key1":"value1","key2":"value2","sub":"subject", "role:tenant1:group1": ["admin", "guest"], "role:tenant1:group2": ["guest"] }`

var cl model.Claims
err := json.Unmarshal([]byte(data), &cl)
require.NoError(t, err)

assert.Len(t, cl.Payload, 2)
assert.Len(t, cl.Payload, 4)
assert.Equal(t, "issuer", cl.Issuer)
assert.Equal(t, "subject", cl.Subject)
assert.Equal(t, "value1", cl.Payload["key1"])
assert.Equal(t, "value2", cl.Payload["key2"])
assert.Contains(t, cl.Payload, "role:tenant1:group1")
assert.Contains(t, cl.Payload["role:tenant1:group1"], "admin")
assert.Contains(t, cl.Payload["role:tenant1:group1"], "guest")
assert.Equal(t, cl.Payload["role:tenant1:group2"], []string{"guest"})
}

// func TestTokenClaimMethod(t *testing.T) {
// token = v
// }
132 changes: 132 additions & 0 deletions server/controller/tenant_test.go
Original file line number Diff line number Diff line change
@@ -1 +1,133 @@
package controller

import (
"context"
"testing"

"github.com/madappgang/identifo/v2/jwt/service"
"github.com/madappgang/identifo/v2/model"
"github.com/madappgang/identifo/v2/storage"
"github.com/madappgang/identifo/v2/storage/mock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestGetInvitationFromClaim(t *testing.T) {
claims := map[string]any{
"some claims": "some value",
"role:tenant1:group1": []any{model.RoleAdmin, model.RoleGuest},
"role:tenant1:group2": []any{model.RoleOwner, model.RoleGuest},
"role:tenant2:group1": []any{model.RoleGuest},
"role:tenant2:group2": "some text",
}

inv := getInvitationFromClaim(claims)

require.NotNil(t, inv)

assert.NotNil(t, inv["tenant1"])
assert.NotEmpty(t, inv["tenant1"].Groups)
assert.Equal(t, 2, len(inv["tenant1"].Groups))
assert.Len(t, inv["tenant1"].Groups["group1"], 2)
assert.Len(t, inv["tenant1"].Groups["group2"], 2)
assert.Contains(t, inv["tenant1"].Groups["group1"], model.RoleAdmin)
assert.Contains(t, inv["tenant1"].Groups["group1"], model.RoleGuest)
assert.Contains(t, inv["tenant1"].Groups["group2"], model.RoleOwner)

assert.NotNil(t, inv["tenant2"])
assert.NotEmpty(t, inv["tenant2"].Groups)
assert.Equal(t, 1, len(inv["tenant2"].Groups))
assert.Len(t, inv["tenant1"].Groups["group1"], 2)
}

func TestFilterInviteeCouldInvite(t *testing.T) {
invitedTo := getInvitationFromClaim(claims)

c := NewUserStorageController(nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, model.DefaultServerSettings)
r := c.filterInviteeCouldInvite(inviterMembership, invitedTo)
require.NotNil(t, r)
require.NotEmpty(t, r)

assert.Len(t, r, 1)
assert.Len(t, r["tenant1"].Groups, 1)
assert.Len(t, r["tenant1"].Groups["group1"], 2)
assert.Contains(t, r["tenant1"].Groups["group1"], model.RoleAdmin)
assert.Contains(t, r["tenant1"].Groups["group1"], model.RoleGuest)
}

func TestAddUserWithInvitationToken(t *testing.T) {
u := mock.UserStorage{
UData: map[string]model.UserData{
"user1": {
UserID: "user1",
TenantMembership: inviterMembership,
},
},
}
user := model.User{
ID: "user1",
GivenName: "Mr Inviter",
Email: "aooth@madappgang.com",
}
newUser := model.User{
ID: "New User",
GivenName: "Mr New USer",
Email: "aooth_new@madappgang.com",
}
ts := createTokenService(t)
token, err := ts.NewToken(model.TokenTypeInvite, user, nil, nil, claims)
require.NoError(t, err)
c := NewUserStorageController(&u, &u, nil, nil, nil, nil, nil, nil, nil, nil, model.DefaultServerSettings)

uu, err := c.AddUserToTenantWithInvitationToken(context.TODO(), newUser, token)
require.NoError(t, err)
assert.NotEmpty(t, uu.TenantMembership)
}

var claims = map[string]any{
"some claims": "some value",
"role:tenant1:group1": []any{model.RoleAdmin, model.RoleGuest},
"role:tenant1:group2": []any{model.RoleOwner, model.RoleGuest},
"role:tenant2:group1": []any{model.RoleGuest},
"role:tenant2:group2": "some text",
}

var inviterMembership = map[string]model.TenantMembership{
"tenant1": {
TenantID: "tenant1",
Groups: map[string][]string{
"group1": {model.RoleGuest, model.RoleAdmin},
"group2": {model.RoleGuest},
},
},
"tenant3": {
TenantID: "tenant3",
Groups: map[string][]string{"group1": {model.RoleGuest, model.RoleAdmin}},
},
}

const (
keyPath = "../../jwt/test_artifacts/private.pem"
testIssuer = "aooth.madappgang.com"
)

func createTokenService(t *testing.T) model.TokenService {
keyStorage, err := storage.NewKeyStorage(model.FileStorageSettings{
Type: model.FileStorageTypeLocal,
Local: model.FileStorageLocal{
Path: keyPath,
},
})
require.NoError(t, err)

privateKey, err := keyStorage.LoadPrivateKey()
require.NoError(t, err)

tokenService, err := service.NewJWTokenService(
privateKey,
testIssuer,
model.DefaultServerSettings.SecuritySettings,
)
require.NoError(t, err)
return tokenService
}
131 changes: 0 additions & 131 deletions server/controller/user_controller_tenant_test.go

This file was deleted.

0 comments on commit bf55979

Please sign in to comment.