@robbiethegeek asked me to write up some thoughts on the API architecture and how it relates to AuthN and AuthZ. So here goes...
For AuthN, there are 2 pieces:
- ID.me session
- www.vets.gov session
For AuthZ, there are 2 pieces:
- Delegated Authorization server/generator
- api.vets.gov authorization consumer/validator
For the API business logic, there are at least 2 pieces:
- Database access to the "Veteran Profile".
- Access to the "task queue."
The above is generic architecture that should be true regardless of underlying technology choice. The only axioms are (a) www.vets.gov and api.vets.gov are 2 different origins (b) id.me is providing identity.
AuthN (authentication)
Input: (maybe) User's username/password
Endpoints: Whatever the authnrequest endpoint in id.me is and https://www.vets.gov/auth/saml-acs-url
Output: A session on the www.vet.gov
origin with identity info that's valid for the session and used by the authz step below.
For (1) id.me is handling the login session. We should verify how this session behaves. 3 user scenarios matter:
- User has no session with id.me and a SAML bounce is initiated from vets.gov. Here, user should be prompted from a password and sent back to vets.gov. If vets.gov session is destroyed (eg www.vets.gov cookies cleared)...and user visits id.me, ensure password is repromtped. The easy architecture mistake here is id.me retains the login session so the user just silently gets regarnted a SAML Assertion.
- User has existing session with id.me (eg, via id.me wallet) and SAML session is initiated from vets.gov. It's unclear what the right behavior here is. By default, I expect the user will just log into vets.gov silently without a password prompt. It's unclear that if we clear the vets.gov session if they'd log out of id.me too. This is the tricky scenario that we need to hammer out with their product team ASAP as it is possibly an architecture level change for their service.
- User has existing session with
www.vets.gov
and logs out of www.vets.gov
. Expected behavior is if they hit login, they will be reprompted for a password. Again, interactions id.me session properties need to be exercised like in the above.
Note that the ACS URL should be on www.vets.gov
and NOT api.vets.gov
. This is because your login session that has your identity is best located along with the webapps itself. If you host on api.vets.gov, this gets a bit harder as you are now creating an additional (unnecessary) cross-origin information share for no real reason.
In the simplest setup, https://www.vets.gov/auth/saml-acs-url
should set a secure https cookie that holds whatever login or identity info is necessary for the app.
If you want to get more complicated, you could store that info client-side only with HTML5 sessionStorage or localStorage...but in all cases, the information is bound to the www.vets.gov
origin and not the api.vets.gov
origin which is conceptually cleaner because api.vets.gov
remains stateless and because you're not poking holes in the origin security boundary between api.vets.gov
and www.vets.gov
.
For a login session, I think cookies or localStorage
are the best ways to store the info. Using server-side sessions feels overkill and places an unnecessarily scaling dependency on the server having a coherent database across all webserver instances (imagine we get a new east region but TIC still bounces people between E/W every 30 seconds via DNS round-robin...you'd have a crazy hard time keeping the session DB consistent).
sessionStorage
is tempting, but not quite right here because of the session forking semantics when a new window is created. If you right-click "open new tab" on a link, the two sessionStorages are now forked and diverge. If you open a new tab and navigate back to www.vets.gov
, there is no way to access the sessionStorage
since the objective is to give tab isolation meaning the user will behave as if they are logged out. sessionStorage
is probably great for transient app state...not so much for login.
For storage of the identity attributes, I would just dump it in a secure cookie unless you have a strong reason to keep the browser from seeing the data...but that's weird since you're basically asking a user not to attack their own PII.
So in summary:
- www.vets.gov creates AuthNRequest to id.me
- id.me returns SAML Assertion with identity attributes to www.vets.gov/auth/saml-acs-url
- an actual app server running on www.vets.gov receives the Assertion, decrypts it, and creates a session on www.vets.gov with the login credentials and identity attributes. This identity attributes should be made available to the javascript context of the react apps but the login session should be protected as usual via secure cookies.
- This login session will then be used in the AuthZ step to generate the delegated authorization for the API server. This is why the login session must exist and be bound to
www.vets.gov
and not api.vets.gov
.
AuthZ (Authorization)
Input: A valid login session, likely in a cookie.
Endpoints: https://www.vets.gov/auth/oauth2
Output: An OAuth2 access token that can be used in the Authorization
header for request to api.vets.gov
NOTE: if you do not have api.vets.gov DNS stood-up yet, I highly suggest creating a heroku app that reverse proxies the API server so you can correctly simulate the API server being on a different origin! If you do not do this, you're gonna run into all sorts of fun with rails sessions colliding. Spinning up a reverse proxy hack should cost you like 1/2 a day but ensure you are developing against the right security contraints.
Rant... There's been a lot of argument about JWT vs Oauth2. It doesn't matter which you use, but I think it's silly to use JWT cause the world uses OAuth2, JWT does not solve the same problem, and the OAuth2 detractors are...well...just the same few people who write shrill blogs. We've spent more time arguing about this than it'd take to write up both prototypes. You can wire up an OAuth2 provider in about 1-2 days and the consumer libraries are tested. Just use OAuth2. It'll keep the API easily compatible with mobile ecosystems and frameworks as well which is a longer term goal. Or if there is really strong resistance still (there shouldn't be. stop arguing for JWT. Oauth2 setup is not complicated...the libraries abstracted ita ll), at least stop discussing whether or not to use it and stand up some real authorization server asap. The devs need to learn how to pass these things around and debug authorization issues which is being blocked by pushing off standing up a real one. If there's a question about this...ask how many devs are thinking about CORS problems or writing wrappers for fetch
in react? If the answer isn't everyone, then there's learning to be had that's being blocked.
...End-Rant
For the MVP, create 1 "scope" (this is just a string you put in the configs for the server and client libraries) named https://www.vets.gov/oauth/scopes/all
or something that is just "all access" for a user. You can create more restricted scopes (eg 'https://www.vets.gov/oauth/scopes/rx-refill', 'https://www.vets.gov/oauth/scopes/secure-messaging') later if there is some meaningful separation. But for now, just one uber access scope makes sense.
After the server is stood up with a configured scope, write it up to a webapp to get an access token after having logged in. Assuming the user has a valid www.vets.gov
session and that the authorization server running doorkeeper has access to this session (hint: use the same app server for the saml acs_url as authorization server so they can share the session secret)
Assuming https://github.com/doorkeeper-gem/doorkeeper gem, I think the following is accurate:
- Client app uses OAuth2 library to generate a authz request for
https://www.vets.gov/oauth/scopes/all
to https://www.vets.gov/auth/oauth2/authorize
https://www.vets.gov/auth/oauth2/authorize
server notices "it is a first party auth" (make the controller skip prompting the user to click "authorize this app" if the request is coming from the registered www.vets.gov application... this flow is useful if a non-vets.gov source like a mobile app wants to use api.vets.gov). This controller now becomes just a check that we have a www.vets.gov
session and is otherwise a no-op.
https://www.vets.gov/auth/oauth2/authorize
returns the authorization code (aka refresh token) to the app. This is given to the oauth-2 client library which will at some point do another post that exchanges the authorization token for an actual "access token" that will be passed to `api.vets.gov.
- Client App has all information needed to create API requests to
api.vets.gov
. We likely want to create a wrapper around fetch
that annotates the access tokens from above onto all API requests in an Authorization:
header.
On the api-server side, add in the right authorization checks with doorkeeper to validate the tokens for the target endpoints.
API business logic
Input: POST/GET to api.vets.gov
with Oauth2 Authorization token.
Endpoints: api.vets.gov/*
Output: Some JSON response and maybe side-effects to backend systems.
This is the easiest portion. The React app should be making API request with just the access token. api.vets.gov
will have to associate in the backend the oauth2 token with the veteran profile. After validation of the access token is completed and the associated profile is found, the authn and authz technologies are done. Now it's just standard business logic checks to do the real authorization.