Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Auth0 AppMetdata Struct #632

Merged
merged 4 commits into from
Jul 12, 2022
Merged
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
8 changes: 8 additions & 0 deletions .env.template
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,10 @@ GDS_BFF_TESTNET_MEMBERS_TIMEOUT=5s
GDS_BFF_TESTNET_MEMBERS_MTLS_CERT_PATH=
GDS_BFF_TESTNET_MEMBERS_MTLS_POOL_PATH=

GDS_BFF_TESTNET_ADMIN_ENDPOINT=http://localhost:4434
GDS_BFF_TESTNET_ADMIN_AUDIENCE=http://localhost:4433
GDS_BFF_TESTNET_ADMIN_TOKEN_KEYS=

GDS_BFF_MAINNET_DIRECTORY_INSECURE=true
GDS_BFF_MAINNET_DIRECTORY_ENDPOINT=localhost:5436
GDS_BFF_MAINNET_DIRECTORY_TIMEOUT=5s
Expand All @@ -167,6 +171,10 @@ GDS_BFF_MAINNET_MEMBERS_TIMEOUT=5s
GDS_BFF_MAINNET_MEMBERS_MTLS_CERT_PATH=
GDS_BFF_MAINNET_MEMBERS_MTLS_POOL_PATH=

GDS_BFF_MAINNET_ADMIN_ENDPOINT=http://localhost:5434
GDS_BFF_MAINNET_ADMIN_AUDIENCE=http://localhost:4433
GDS_BFF_MAINNET_ADMIN_TOKEN_KEYS=

GDS_BFF_DATABASE_URL=trtl://localhost:4436/
GDS_BFF_DATABASE_REINDEX_ON_BOOT=false
GDS_BFF_DATABASE_INSECURE=true
Expand Down
6 changes: 3 additions & 3 deletions pkg/bff/admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,11 +80,11 @@ func (s *Server) Certificates(c *gin.Context) {
}

// Extract the VASP IDs from the claims
// Note that if testnet or mainnet are absent from the VASPs map, the ID will
// Note that if testnet or mainnet are absent from the VASPs struct, the ID will
// default to an empty string, and GetCertificates will return nil for that network
// instead of an error.
testnetID := claims.VASPs[testnet]
mainnetID := claims.VASPs[mainnet]
testnetID := claims.VASPs.TestNet
mainnetID := claims.VASPs.MainNet
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes a lot more sense than indexing into the map, can you update the above comment to reflect that this is a struct with default values now rather than a map?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!


// Get the certificate replies from the admin APIs
testnet, mainnet, testnetErr, mainnetErr := s.GetCertificates(c.Request.Context(), testnetID, mainnetID)
Expand Down
10 changes: 5 additions & 5 deletions pkg/bff/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,11 @@ var AnonymousClaims = Claims{Scope: ScopeAnonymous, Permissions: nil}

// Claims extracts custom data from the JWT token provided by Auth0
type Claims struct {
Scope string `json:"scope"`
Permissions []string `json:"permissions"`
OrgID string `json:"https://vaspdirectory.net/orgid"`
VASPs map[string]string `json:"https://vaspdirectory.net/vasps"`
Email string `json:"https://vaspdirectory.net/email"`
Scope string `json:"scope"`
Permissions []string `json:"permissions"`
OrgID string `json:"https://vaspdirectory.net/orgid"`
VASPs VASPs `json:"https://vaspdirectory.net/vasps"`
Email string `json:"https://vaspdirectory.net/email"`
}

// Validate implements the validator.CustomClaims interface for Auth0 parsing.
Expand Down
2 changes: 1 addition & 1 deletion pkg/bff/auth/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ func TestClaimsContext(t *testing.T) {
bclaims, err := auth.GetClaims(c)
require.NoError(t, err, "could not fetch bff claims")
require.Equal(t, "6f0d943d-6cd7-4745-bc9d-6d65e32c70e9", bclaims.OrgID)
require.Equal(t, "eee784b5-49b3-452e-97d5-1b01e79f5e62", bclaims.VASPs["testnet"])
require.Equal(t, "eee784b5-49b3-452e-97d5-1b01e79f5e62", bclaims.VASPs.TestNet)
require.True(t, bclaims.HasAllPermissions("add:collaborators", "read:certificates"))

rclaims, err := auth.GetRegisteredClaims(c)
Expand Down
45 changes: 45 additions & 0 deletions pkg/bff/auth/metadata.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package auth

import "encoding/json"

// AppMetadata makes it easier to serialize and deserialize JSON from the auth0
// app_metadata assigned to the user by the BFF (and ensures the data is structured).
type AppMetadata struct {
OrgID string `json:"orgid"`
VASPs VASPs `json:"vasps"`
}

type VASPs struct {
MainNet string `json:"mainnet"`
TestNet string `json:"testnet"`
}

func (meta *AppMetadata) Load(appdata map[string]interface{}) (err error) {
// Serialize appdata back to JSON
var data []byte
if data, err = json.Marshal(appdata); err != nil {
return err
}

// Deserialize app metadata from struct
if err = json.Unmarshal(data, meta); err != nil {
return err
}

return nil
}

func (meta *AppMetadata) Dump() (appdata map[string]interface{}, err error) {
// Serialize meta back to JSON
var data []byte
if data, err = json.Marshal(meta); err != nil {
return nil, err
}

appdata = make(map[string]interface{})
if err = json.Unmarshal(data, &appdata); err != nil {
return nil, err
}

return appdata, nil
}
109 changes: 109 additions & 0 deletions pkg/bff/auth/metadata_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package auth_test

import (
"testing"

"github.com/stretchr/testify/require"
. "github.com/trisacrypto/directory/pkg/bff/auth"
)

func TestAppMetadata(t *testing.T) {
// Test loading and dumping app_metadata to/from the auth0 response.
testCases := []struct {
appdata map[string]interface{}
expected *AppMetadata
}{
{
nil, &AppMetadata{},
},
{
map[string]interface{}{}, &AppMetadata{},
},
{
map[string]interface{}{
"orgid": "67428be4-3fa4-4bf2-9e15-edbf043f8670",
},
&AppMetadata{
OrgID: "67428be4-3fa4-4bf2-9e15-edbf043f8670",
},
},
{
map[string]interface{}{
"vasps": map[string]string{
"testnet": "1bcacaf5-4b43-4e14-b70c-a47107d3a56c",
},
},
&AppMetadata{
VASPs: VASPs{
TestNet: "1bcacaf5-4b43-4e14-b70c-a47107d3a56c",
},
},
},
{
map[string]interface{}{
"vasps": map[string]string{
"mainnet": "2ac8d50a-ff4c-479e-8eec-a35d96d90911",
},
},
&AppMetadata{
VASPs: VASPs{
MainNet: "2ac8d50a-ff4c-479e-8eec-a35d96d90911",
},
},
},
{
map[string]interface{}{
"vasps": map[string]string{
"testnet": "1bcacaf5-4b43-4e14-b70c-a47107d3a56c",
"mainnet": "2ac8d50a-ff4c-479e-8eec-a35d96d90911",
},
},
&AppMetadata{
VASPs: VASPs{
TestNet: "1bcacaf5-4b43-4e14-b70c-a47107d3a56c",
MainNet: "2ac8d50a-ff4c-479e-8eec-a35d96d90911",
},
},
},
{
map[string]interface{}{
"orgid": "67428be4-3fa4-4bf2-9e15-edbf043f8670",
"vasps": map[string]string{
"testnet": "1bcacaf5-4b43-4e14-b70c-a47107d3a56c",
"mainnet": "2ac8d50a-ff4c-479e-8eec-a35d96d90911",
},
},
&AppMetadata{
OrgID: "67428be4-3fa4-4bf2-9e15-edbf043f8670",
VASPs: VASPs{
TestNet: "1bcacaf5-4b43-4e14-b70c-a47107d3a56c",
MainNet: "2ac8d50a-ff4c-479e-8eec-a35d96d90911",
},
},
},
}

for _, tc := range testCases {
actual := &AppMetadata{}

err := actual.Load(tc.appdata)
require.NoError(t, err, "could not load appdata")
require.Equal(t, tc.expected, actual, "app_metadata did not load correctly")

appdata, err := actual.Dump()
require.NoError(t, err, "could not dump app_metdata")

require.Contains(t, appdata, "orgid")
require.Equal(t, actual.OrgID, appdata["orgid"])

require.Contains(t, appdata, "vasps")
vasps, ok := appdata["vasps"].(map[string]interface{})
require.True(t, ok, "appdata vasps is wrong type")

require.Contains(t, appdata["vasps"], "testnet")
require.Equal(t, actual.VASPs.TestNet, vasps["testnet"])
require.Contains(t, appdata["vasps"], "mainnet")
require.Equal(t, actual.VASPs.MainNet, vasps["mainnet"])
}

}
4 changes: 2 additions & 2 deletions pkg/bff/members.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,8 @@ func (s *Server) Overview(c *gin.Context) {
}

// Extract the VASP IDs from the claims
testnetID := claims.VASPs[testnet]
mainnetID := claims.VASPs[mainnet]
testnetID := claims.VASPs.TestNet
mainnetID := claims.VASPs.MainNet

out := api.OverviewReply{
OrgID: claims.OrgID,
Expand Down
90 changes: 29 additions & 61 deletions pkg/bff/users.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,14 @@ func (s *Server) Login(c *gin.Context) {

// Ensure the user resources are correctly populated.
// If the user is not associated with an organization, create it.
if orgID, ok := GetOrgID(user.AppMetadata); !ok || orgID == "" {
appdata := &auth.AppMetadata{}
if err = appdata.Load(user.AppMetadata); err != nil {
log.Error().Err(err).Msg("could not parse user app metadata")
c.JSON(http.StatusInternalServerError, "could not parse user app metadata")
return
}

if appdata.OrgID == "" {
// Create the organization
org, err := s.db.Organizations().Create(c.Request.Context())
if err != nil {
Expand All @@ -78,39 +85,29 @@ func (s *Server) Login(c *gin.Context) {
}

// Set the organization ID in the user app metadata
user.AppMetadata[OrgIDKey] = org.Id
if err = s.auth0.User.Update(*user.ID, user); err != nil {
log.Error().Err(err).Msg("could not update user app_metadata")
c.JSON(http.StatusInternalServerError, "could not complete user login")
return
}
appdata.OrgID = org.Id
} else {
// Get the organization for the specified user
org, err := s.db.Organizations().Retrieve(c.Request.Context(), orgID)
org, err := s.db.Organizations().Retrieve(c.Request.Context(), appdata.OrgID)
if err != nil {
log.Error().Err(err).Msg("could not retrieve organization for user VASP verification")
log.Error().Err(err).Str("orgid", appdata.OrgID).Msg("could not retrieve organization for user VASP verification")
c.JSON(http.StatusInternalServerError, "could not complete user login")
return
}

// Create the actual VASP table
directory := make(map[string]string)
// Ensure the VASP record is correct for the user
if org.Testnet != nil && org.Testnet.Id != "" {
directory[testnet] = org.Testnet.Id
appdata.VASPs.TestNet = org.Testnet.Id
}
if org.Mainnet != nil && org.Mainnet.Id != "" {
directory[mainnet] = org.Mainnet.Id
appdata.VASPs.MainNet = org.Mainnet.Id
}
}

// Ensure the VASP record is correct for the user
if vasps, ok := GetVASPs(user.AppMetadata); !ok || !MapEqual(vasps, directory) {
user.AppMetadata[VASPsKey] = directory
if err = s.auth0.User.Update(*user.ID, user); err != nil {
log.Error().Err(err).Msg("could not update user app_metadata")
c.JSON(http.StatusInternalServerError, "could not complete user login")
return
}
}
if err = s.SaveAuth0AppMetadata(*user.ID, *appdata); err != nil {
log.Error().Err(err).Str("user_id", *user.ID).Msg("could not save user app_metadata")
c.JSON(http.StatusInternalServerError, "could not complete user login")
return
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we are now syncing the user app metadata at the end, this protects us from partial updates?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There should only be two possible cases:

  1. The user is logging in for the first time and an organization is created for them
  2. The user needs the VASP ids updated from their organization

In case #1 - the following data is saved:

{
  "orgid": "uuid",
  "vasps": {
    "testnet": "",
    "mainnet": "",
  }
}

Case #1 should only happen once.

Because I moved the syncing to the end, case #2 is going to happen on every single login since I removed the MapEqual check. I debated whether or not we should do this -- it is safer, and logins are infrequent. However, we will eventually need some change detection to alert the front-end that the user needs to login again; so this is likely temporary.

}

// Protect the front-end by setting double cookie tokens for CSRF protection.
Expand Down Expand Up @@ -140,48 +137,19 @@ func (s *Server) FindRoleByName(name string) (*management.Role, error) {
return nil, fmt.Errorf("could not find role %q in %d available roles", name, len(roles.Roles))
}

func GetOrgID(appdata map[string]interface{}) (orgID string, ok bool) {
var val interface{}
if val, ok = appdata[OrgIDKey]; !ok {
return "", false
}
func (s *Server) SaveAuth0AppMetadata(uid string, appdata auth.AppMetadata) (err error) {
// Create a blank user with no data but the appdata
user := &management.User{}

if orgID, ok = val.(string); !ok {
return "", false
// Send the updated user app_metadata back to auth0
if user.AppMetadata, err = appdata.Dump(); err != nil {
return err
}

return orgID, true
}

func GetVASPs(appdata map[string]interface{}) (vasps map[string]string, ok bool) {
var val interface{}
if val, ok = appdata[VASPsKey]; !ok {
return nil, false
}

if vasps, ok = val.(map[string]string); !ok {
return nil, false
}

return vasps, true
}

func MapEqual(a, b map[string]string) bool {
if len(a) != len(b) {
return false
}

for key, val := range a {
if alt, ok := b[key]; !ok || val != alt {
return false
}
}

for key, val := range b {
if alt, ok := a[key]; !ok || val != alt {
return false
}
// Patch the user with the specified user ID
if err = s.auth0.User.Update(uid, user); err != nil {
return err
}

return true
return nil
}
Loading