luillo1 / RaceResults

A platform for managing and sharing running race results within running groups
4 stars 0 forks source link

Ideas for org member authentication #80

Open MutatedGamer opened 2 years ago

MutatedGamer commented 2 years ago

To improve our ability to scale to different organizations, I propose the following classes, endpoints and FE/BE interaction

  1. 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.

  2. 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;
    }
  3. 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})
  4. 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

      orgAssignedMemberId: "56789",
      requiredHeaders: [
      {"WA-Access-Token" : "XXXXXXX" },
      {"WA-AccountId" : "1234"}
      ]

    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.

  5. 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.
  6. 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.