Boemska / create-sas-app

Set up a modern SAS backed web app by running just a couple of commands.
Apache License 2.0
70 stars 18 forks source link

Integrate restaf / restaflib with CSA #6

Open boomskats opened 4 years ago

boomskats commented 4 years ago

This is a placeholder to track integration of @devaKumaraswamy's excellent restaf + restaflib into CSA, as an enhancement to our own managedRequest API access mechanism.

Looking at the authentication options it should be possible to configure axios with an intereceptor to pause requests while re-authentication happens, and I quite like the idea of moving h54s to axios in general for the next version - it does a lot of things elegantly which we implemented from scratch within h54s (because axios didn't exist at the time).

We need to think about how we best do it - whether to just include restaf here as a branch that we integrate with the login screen and keep the current auth + keepalive mechanisms, or wait until h54s v3 where we can share axios with restaf. Just putting this out there for now.

devaKumaraswamy commented 4 years ago

Nik: I started messing around with adding restaf to your app.

  1. Is there a way to get to the viya url in the app.js?
  2. Is the authentication authorization_code flow?

Cheers... Deva

boomskats commented 4 years ago

Ok, so to answer the URL question - there is a property within the H54S adapter instance which you can read if it's explicitly set, something like hostUrl, but we leave that as blank nowadays as our apps are typically served from the same domain as the APIs. If you need to set a server URL explicitly in your code you can pull it from window.location, but assume that our apps are always served from behind the same reverse proxy as the APIs you want to talk to.

On the flow question, I think the answer is 'yes', but possibly not a simple one. I've seen a couple of different definitions of the auth code flow for javascript webapps within the Viya docs, so I guess it's a good idea to write down how our auth mechanism works and fits in within those definitions. Sorry about the wall of text, but I figure it's worth doing - if it makes sense we can probably stick it in the Wiki.

So like I said, short answer is yes - the auth flow in question is the Authorization Code flow. But it's also no, as I don't think our apps 'use' that flow as such. We are just a WebAuth session-based client to the OAuth clients (sas.identities, sas.files, sas.folders, sas.casManagement, sas.SASJobExecution, etc.).

I think it is the idea described in the third paragraph here (from https://tools.ietf.org/html/draft-ietf-oauth-browser-based-apps-06):

Parecki & Waite          Expires October 7, 2020                [Page 5]

Internet-Draft      OAuth 2.0 for Browser-Based Apps          April 2020

6.1.  Browser-Based Apps that Can Share Data with the Resource Server

   For simple system architectures, such as when the JavaScript
   application is served from a domain that can share cookies with the
   domain of the API (resource server), OAuth adds additional attack
   vectors that could be avoided with a different solution.
   In particular, using any redirect-based mechanism of obtaining an
   access token enables the redirect-based attacks described in
   [oauth-security-topics], but if the application, authorization server
   and resource server share a domain, then it is unnecessary to use a
   redirect mechanism to communicate between them.

   An additional concern with handling access tokens in a browser is
   that as of the date of this publication, there is no secure storage
   mechanism where JavaScript code can keep the access token to be later
   used in an API request.  Using an OAuth flow results in the
   JavaScript code getting an access token, needing to store it
   somewhere, and then retrieve it to make an API request.

   Instead, a more secure design is to use an HTTP-only cookie between
   the JavaScript application and API so that the JavaScript code can't
   access the cookie value itself.  Additionally, the SameSite cookie
   attribute can be used to prevent CSRF attacks, or alternatively, the
   application and API could be written to use anti-CSRF tokens.

   OAuth was originally created for third-party or federated access to
   APIs, so it may not be the best solution in a common-domain
   deployment.  That said, using OAuth even in a common-domain
   architecture does mean you can more easily rearchitect things later,
   such as if you were to later add a new domain to the system.

Our apps are a common-domain deployment that depend on built-in browser-based security mechanisms - i.e. those HTTP-only cookies that maintain a webauth session with the actual OAuth clients. They use cookies that our javascript code can never actually read (as described above, the SAS cookies are HTTP-only and served from the same domain, scoped to the URIs of the individual microservices). The text above suggests that this mechanism is more secure than using OAuth, and it comes with the added bonus of zero configuration from the SAS side.

The best description I've ever read of this is the Authorization Code flow in Mike Roda's paper from 2018 (https://www.sas.com/content/dam/SAS/support/en/sas-global-forum-proceedings/2018/1737-2018.pdf). In this context, we are just the visual application running in the browser:

Authorization Code

Very much the preferred flow, the authorization code flow is sometimes referred to as the '3-legged' flow since it involves a 3-way conversation between a client, the user, and the authorization server. The flow facilitates the client obtaining an access token without ever handling the user's credentials. In the context of SAS Viya, this flow is used when a visual application running in the browser (for example, SAS VisualAnalytics) calls an API. SAS Viya builds on this flow with session management. Here are the steps:

  1. The application running in the browser instructs the browser to make a request to a REST API.

  2. The service hosting the API responds with an HTTP session cookie and a redirect to the/SASLogon/oauth/authorize endpoint on SASLogon.

  3. The browser remembers the session cookie and calls SASLogon with the authorization request.

  4. SASLogon receives the authorization request. If the request is coming from a SAS client, the request is automatically approved for all scopes (group memberships) and responds with a new authorization code and redirects the browser back to the URI of the API.

  5. The browser sends the request with the authorization code to the API. The request also containsthe session cookie.

  6. The API service sends a request to the /SASLogon/oauth/token endpoint and passes the authorization code.

  7. SASLogon issues a new access token and ID token and returns them in the response, along witha refresh token. A special claim is added to the access token to associate it with the login session.

  8. The API processes the tokens to authenticate the user and make an authorization decision based on the scopes (group memberships) in the access token. The tokens are stored in the HTTP session and another redirect is returned to the browser to call the API without the authorization code. This last redirect isn't technically necessary but ensures that the code isn't left in the navigation bar where it might get bookmarked.

  9. The browser receives the redirect and calls the API again without the authorization code.

  10. The API returns the data requested.

  11. The browser receives the data from the API and hands it to the visual application.

These steps assume that the end user is already signed in to SASLogon and has an active HTTP session with SASLogon. If not, SASLogon redirects the browser to the login page after step 3 so that the user can sign in and establish an HTTP session. Note that each service returns HTTP session cookies with a path, so the browser maintains individual HTTP sessions with each service it has talked to. Subsequent calls from the browser to the same API use the existing HTTP session without triggering the authorization code flow again. This happens until the session times out from inactivity, which is 30 minutes by default. If the API call is not an HTTP GET or HEAD request, some additional steps happen since the original HTTP method and any body content are lost after following a 302 redirect. The Location URI returned tothe browser in step 2 includes a callback URI to the API plus a parameter indicating the original HTTP method. In step 8 the API returns a special HTTP 449 response instead of 302. HTTP 449 is a Microsoft extension for Retry. The application in the browser gets the 449 and issues a retry of the original request in step 1. This time the request goes through because a session has been established.

Now, this makes things a bit more interesting from a library implementation perspective.

Because we are just the dumb browser useragent javascript webapp that has no visibility of the validity of session cookies the browser currently holds for any of our API endpoints, we need a mechanism that handles the ajax redirects to SASLogon if and when we need to reauthenticate for an API request, and a way of handling any resulting HTTP 449 responses (as described in the paper excerpt above).

The way we have always handled this with our adapter, for both v9 and Viya, is as follows:

Our adapter doesn't expect the developer to implement an explicit 'lets authenticate' step. Instead developers implement all their API requests using our managedRequest wrapper, which allows them to simply assume that request will eventually be satisfied, even if it is redirected to SASLogon. That managedRequest xhr wrapper wraps those requests to the APIs, and then handles the request for user authentication if it detects that a response came back as a SASLogon redirect.

In practice, even though there's no 'let's authenticate' step, users do tend to see the logon screen as soon as they start the app because as soon as the app starts it tries to fetch the userAvatar from the /identities service, which then gets redirected to SASLogon and triggers that Login routine (unless the user is logged in to another SAS app in another tab in which case they don't have to do anything). When a request is redirected to SASLogon in the background, such as when a session expires, the app locks the UI and prompts the user for a password. The callback for the original request that failed with a redirect to SASLogon gets pushed to a stack, and is resumed/retried and satisfied once we have a newly authenticated session. This is how it works both on SAS v9 and on Viya, for both job calls to the JES and for any API calls using the 'managedRequest'.

So what does this mean for restaf integration

In the same way you use axios to talk to the APIs, we use our adapter's managedRequest wrapper around the XMLHTTPRequest. In the same way that we detect redirects to SASLogon, we could use a response interceptor in axios to check whether one of your requests came back as a redirect to SASLogon, and when it happens, trigger the CSA built in login handler locking / prompt user for password routine that asks them to reauthenticate. The only thing we have to ensure if we do things this way is that there's a mechanism within restaf for handling HTTP 449 retry original request responses, and I don't know how difficult that would be to do from your side, or whether it is something we can build into axios - if it can retry the request of the response that was intercepted. The result would be very cool though.

That aside, I think initially you should just be able to work with the browser session that the app obtains on startup? As long as you have a way of turning off the authorisation headers in the requests so that they don't override the session-based auth that's already in place.

This response is so long winded and repetitive. I might edit it later. But I hope it makes sense.