To improve our ability to scale to different organizations, I propose the following classes, endpoints and FE/BE interaction
Create a new abstract Auth model. The abstract class would look like
public abstract class Auth : IModel
{
public Guid OrganizationId;
public abstract AuthType AuthType;
}
The AuthType is an enum that is used by the FE/BE to understand what type of authentication flow to use.
Create a new WildApricotAuth model
public class WildApricotAuth : Auth
{
public string Domain;
public string ClientId;
public override AuthType AuthType => AuthType.WildApricot;
}
The ClientSecret will be stored in AKV (more on this later).
For development purposes, we may also want to create a RaceResultsAuth model:
public class RaceResultsAuth : Auth
{
public override AuthType AuthType => AuthType.RaceResults;
}
Create the endpoint GET organizations/{orgId}/auth. This endpoint returns an Auth model. When a user navigates to a page that requires organization authentication, this endpoint will be hit to initiate an authentication flow.
The FE will make a decision on how to continue the auth flow based on the AuthType received.
For AuthType.RaceResults, we use the react MSAL library to ensure we're logged into an admin account
For AuthType.WildApricot, the user is redirected to the appropriate endpoint to receive an authorization_code (i.e. {response.Domain}/sys/login/OAuthLogin?client_Id={response.ClientId})
Create the endpoint POST organizations/{orgId}/auth. This endpoint is called by the FE after the GET request above. The data POSTed depends on the AuthType above.
For AuthType.RaceResults, the POST body is empty since MSAL ensures we send authentication info to our backend.
For AuthType.WildApricot, the POST body is the authorization_code returned by the SSO redirect on the FE.
The BE attempts to authenticate the user here.
For AuthType.RaceResults, we can easily just check User.Identity.IsAuthenticated (there may be a better way to check with an AAD library, not sure).
For AuthType.WildApricot, we POST the Wild Apricot API with the authorization_code. To do this, we'll need the client secret. In the spirit of needing to "on-board" organizations, we will keep all client secrets stored in-memory and have them loaded on app launch.
This endpoint responds with two items: the OrgAssignedMemberId for the logged in user and an IEnumerable<KeyValuePair<string, string>>. The FE will store these key value pairs and add them as headers to subsequent calls to the BE.
For AuthType.RaceResults, the response will look like
orgAssignedMemberId: "56789",
requiredHeaders: []
For AuthType.WildApricot, the response will look like
With this reply, subsequent calls will have "WA-Access-Token" and "WA-AccountId" in their headers. The orgAssignedMemberId is also stored and used to determine which endpoints to call later. Note that the OAMD may not exist as a Member model in our database at this point. This value comes from whatever authentication provider is being used, NOT from our database.
Create a new RequireOrganizationAuthorizationAttribute that we can put on endpoints and controllers. This attribute will lookup the Auth model for the organization being accessed and use request headers to verify the user is authenticated.
For AuthType.RaceResults, this attribute is the same as RequireAuthorization.
For AuthType.WildApricot, the attribute will just verify the WA-Access-Token is valid.
Create a similar RequireOrganizationAuthorizationAttribute(string memberId).
For AuthType.RaceResults, this again will be the same as RequireAuthorization (i.e. admins can impersonate members).
For AuthType.WildApricot, the attribute will get information from Wild Apricot about the logged in user (by using the WA-AccountId and WA-Access-Token headers) and verify the OrgAssignedMemberId matches the Member.OrgAssignedMemberId for the Member model with the specified memberId.
A corollary to this is I think we should avoid the FE needing to interact with other APIs. I made the FE talk to the Wild Apricot API to get information about the authenticated user in order to create a new Member model. I propose making a new POST organizations/{orgId}/members that does NOT take in a member model, but just an orgAssignedMemberId. This route would be protected by a RequireOrganizationAuthorizationAttribute. The BE would figure out what API call needs to be made to fetch info for the logged in user (e.g. calling a Wild Apricot endpoint), create the model, and give it back to the FE.
This idea is a separate discussion/change and would happen independently from the auth proposals above.
To improve our ability to scale to different organizations, I propose the following classes, endpoints and FE/BE interaction
Create a new abstract
Auth
model. The abstract class would look likeThe
AuthType
is an enum that is used by the FE/BE to understand what type of authentication flow to use.Create a new
WildApricotAuth
modelThe
ClientSecret
will be stored in AKV (more on this later).For development purposes, we may also want to create a
RaceResultsAuth
model:Create the endpoint GET
organizations/{orgId}/auth
. This endpoint returns anAuth
model. When a user navigates to a page that requires organization authentication, this endpoint will be hit to initiate an authentication flow.The FE will make a decision on how to continue the auth flow based on the
AuthType
received.AuthType.RaceResults
, we use the react MSAL library to ensure we're logged into an admin accountAuthType.WildApricot
, the user is redirected to the appropriate endpoint to receive anauthorization_code
(i.e.{response.Domain}/sys/login/OAuthLogin?client_Id={response.ClientId}
)Create the endpoint POST
organizations/{orgId}/auth
. This endpoint is called by the FE after the GET request above. The data POSTed depends on theAuthType
above.AuthType.RaceResults
, the POST body is empty since MSAL ensures we send authentication info to our backend.AuthType.WildApricot
, the POST body is theauthorization_code
returned by the SSO redirect on the FE.The BE attempts to authenticate the user here.
AuthType.RaceResults
, we can easily just checkUser.Identity.IsAuthenticated
(there may be a better way to check with an AAD library, not sure).AuthType.WildApricot
, we POST the Wild Apricot API with theauthorization_code
. To do this, we'll need the client secret. In the spirit of needing to "on-board" organizations, we will keep all client secrets stored in-memory and have them loaded on app launch.This endpoint responds with two items: the
OrgAssignedMemberId
for the logged in user and anIEnumerable<KeyValuePair<string, string>>
. The FE will store these key value pairs and add them as headers to subsequent calls to the BE.For
AuthType.RaceResults
, the response will look likeFor
AuthType.WildApricot
, the response will look likeWith this reply, subsequent calls will have "WA-Access-Token" and "WA-AccountId" in their headers. The orgAssignedMemberId is also stored and used to determine which endpoints to call later. Note that the OAMD may not exist as a Member model in our database at this point. This value comes from whatever authentication provider is being used, NOT from our database.
Create a new
RequireOrganizationAuthorizationAttribute
that we can put on endpoints and controllers. This attribute will lookup theAuth
model for the organization being accessed and use request headers to verify the user is authenticated.AuthType.RaceResults
, this attribute is the same asRequireAuthorization
.AuthType.WildApricot
, the attribute will just verify theWA-Access-Token
is valid.Create a similar
RequireOrganizationAuthorizationAttribute(string memberId)
.AuthType.RaceResults
, this again will be the same asRequireAuthorization
(i.e. admins can impersonate members).AuthType.WildApricot
, the attribute will get information from Wild Apricot about the logged in user (by using theWA-AccountId
andWA-Access-Token
headers) and verify the OrgAssignedMemberId matches theMember.OrgAssignedMemberId
for theMember
model with the specifiedmemberId
.A corollary to this is I think we should avoid the FE needing to interact with other APIs. I made the FE talk to the Wild Apricot API to get information about the authenticated user in order to create a new Member model. I propose making a new POST
organizations/{orgId}/members
that does NOT take in a member model, but just anorgAssignedMemberId
. This route would be protected by aRequireOrganizationAuthorizationAttribute
. The BE would figure out what API call needs to be made to fetch info for the logged in user (e.g. calling a Wild Apricot endpoint), create the model, and give it back to the FE.This idea is a separate discussion/change and would happen independently from the auth proposals above.