TwinePlatform / twine-monolith

⬛️ Monorepo for the Twine platform
https://twine-together.com
GNU Affero General Public License v3.0
5 stars 3 forks source link

OAuth integration #415

Open eliasmalik opened 4 years ago

eliasmalik commented 4 years ago

As per the authentication instructions on the Eventbrite API docs, there are two options for OAuth: server-side and client-side. The latter is simpler, but the former is recommended by Eventbrite because it doesn't expose the user's API token in the URL (how significant an issue this is is up for debate: probably depends on whether the user/CB has any personally identifiable information on their Eventbrite account). This issue briefly describes a way to implement both solutions. The choice between them is a trade-off that can be made at a later date.

Server side

We need a redirect URI, which is to say a URL that Eventbrite can redirect clients to after they've authenticated them.

Two options for this:

  1. Add it to the client (i.e. a client side route)
  2. Add an endpoint to the server

Client-side redirect URI

The client then needs to capture the authorization code from the query parameter, and send a request to the API server so it can continue the process of requesting the users access token by calling POST https://eventbrite.com/oauth/token.

The API server will then receive the access token in the response from Eventbrite, which it can store in the DB (along with a refresh token, if present), and send a successful response back to the client.

The client can then enable functionality that relies on the Eventbrite integration, although data requests should probably be proxied through the API server since those requests require the access token, which shouldn't be sent to the client (otherwise server-side authorization is pointless).

Server-side redirect URI

The endpoint needs to encode the originating app or URL (so we know where to redirect the client back to after we're done). This can be done either as a URL parameter (e.g. /oauth/visitor-app/redirect) or as a query parameter (e.g. /oauth/redirect?target=visitor-app or /oauth/redirect?target=https://visitor.twine-together.com/admin/settings URL encoded).

Once the client is redirected to (for example) https://api.twine-together.com/oauth/redirect?target=TARGET_URL&code=AUTH_CODE, the API server completes the process of requesting the access token as above, which it again can store in the DB.

The client continues as above.

Note: this solution requires the cookie settings to be updated to use isSameSite: 'Lax' because we want the cookie to be sent when returning to the domain from a third party domain.

Client side

In this mode, the client receives the access token directly from Eventbrite, which then needs to be sent on to the API server so it can be stored and used later on.

After this is done, we have two options:

  1. Proxy all data requests through the API
  2. Client requests the access token from the API and handles all 3rd party requests (i.e. to the Eventbrite API) directly

Option one reduces the number of times the access token is exposed to the client to one per-authentication. Option two potentially reduces the amount of complexity added to the API server by removing the need for a proxy (although, depending on how this proxy is set-up, this may well be fairly minimal).

eliasmalik commented 4 years ago

Design

Whether or not the server- or client-side solution is chosen, there'll need to be some new endpoints on the backend. It's not clear that there's a benefit to versioning these: OAuth is a fixed standard, it's not going to change. So these endpoints probably don't need to be nested under /v1/... and can instead live at something like /oauth/....

In addition, despite the fact that Eventbrite is the only integration currently being considered, every OAuth-integration will be structured similarly, so it's worth designing a solution that can easily accommodate new integrations, if and when they are desired.

Server-side solution

Server-side redirect URI

In order to make this flexible, it's probably worth identifying the 3rd party service in the URI so new services can be accommodated simply:

/oauth/{service}/redirect?code=AUTH_CODE&target=TARGET_URL

i.e.

/oauth/eventbrite/redirect?code=AUTH_CODE&target=TARGET_URL

There's several common operations that need to be done in the handler for this URI:

  1. Using the authorization code to retrieve the access token
  2. Storing the access token against the user
  3. Redirecting the client back to the page they came from (e.g. visitor settings page). This should maybe include a query param that the client app can use to display a notice that the authentication/authorization was successful

It may not be worth doing during the first integration, but if/when further ones are required, these operations can be pulled into an Eventbrite module that has a common interface as all other OAuth modules (example below). This way, the appropriate module can be selected dynamically based on the path parameter {service}, but the method calls will all be the same.

interface OAuthService {
  fetchAccessToken (code: string): Promise<string>;
  saveAccessToken (token: string, user: User): Promise<void>;
  getRedirectUrl (request: Hapi.Request): string;
}

Client-side redirect URI

In this case we'll need an endpoint that's used to get the access token given an authorization code. This can be a simple authenticated endpoint that requires the auth code as a payload parameter. Again, we'll probably want to encode the service in the URL in a similar manner to above (e.g. POST /oauth/{service}/code), and again, the same 3 common operations identified above will need to be performed in this endpoint.

Client-side solution

In this case we'll need an endpoint that's used to store the access token, which can be a simple authenticated endpoint that requires the token as a payload parameter. Again, we'll probably want to encode the service in the URL in a similar manner to above (e.g. POST /oauth/{service}/tokens). In contrast to the other two solutions, this endpoint will only need to store the token for later use.

In addition, if it is elected not to use a proxy in this solution, we'll need another endpoint to return the access token to the client.

Common elements

In all solutions there needs to be endpoints for the client to be able to fetch the client ID, and potentially also the redirect URI if using the server-side URI.

In server-side solutions, Eventbrite data requests should be proxied through the API server. There are plenty of reverse proxy libraries available, including the hapi-provided h2o2. This can be structured so that any requests to /oauth/{service}/{*param} are forwarded to that services base-URL (e.g. /oauth/eventbrite/users/me is forwarded to https://eventbriteapi.com/v3/users/me). This forwarded request also needs to include the access token in the authorization header (as specified by the eventbrite API docs). This can be done using h2o2's mapUri option.

Client-side solutions can also use a proxy, or use the access token to make API calls directly from the client.