-
Notifications
You must be signed in to change notification settings - Fork 5
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
Changes from 3 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 | ||
} |
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"]) | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 { | ||
|
@@ -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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There should only be two possible cases:
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 |
||
} | ||
|
||
// Protect the front-end by setting double cookie tokens for CSRF protection. | ||
|
@@ -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 | ||
} |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done!