Overview of a Rails project with user management & authentication to proxy serve a private, single-page app.
JFMK-Auth is an open source, personal web app that securely serves an instance of my demo work portfolio to an authenticated, private audience. While it’s not a new or unique problem, the project gave me a testing ground to work with Rails 5.0 & Docker Compose 3. This post walks through what I feel are technical takeaways from the development process.
TLDR; See github.com/jfroom/jfmk-auth for the codebase.
- Uses Rails 5.0 to build a user management, authentication, and login notification system
- ‘Proxy’ serves a private, static, single-page app off S3
- Uses Docker Compose 3 in Development & CI environments — with Selenium WebDriver in a Docker service
- Deploys staging, production, and demo instances to Heroku Pipelines with GitHub and TravisCI integrations
- Open source, configurable, and documented
Docker Compose makes it convenient to run an identical copy of an application, it’s dependencies, and services in a contained, consistent and reliable manner across environments such as: developer workstations, CI servers, and production. This can be a boon to productivity, onboarding new developers, and chasing down issues.
This project focuses on using Compose for the development, test & CI aspects. (I chose Heroku for production — and as of 3/2017, Heroku’s Docker support support is still in beta—so am waiting for that to mature for production docker deployments. Instead I leverage a traditional Heroku build push off TravisCI — more below.)
While learning how to split a Rails app into ‘dockerized’ services for dev, I found a few pain points—and wanted to document my insights into two tangent posts:
Docker Compose & Ruby Bundler Caching in Dev. Save development time by using a Docker entrypoint & volume to persist Bundler’s cache across builds & Gemfile changes.
Docker Compose & Capybara Selenium Standalone. How to configure, run, and debug Capybara tests inside of a Selenium Standalone Docker Compose service.
Base Rails App
I’ve been working with Rails since 2013—but mostly in the context of completing front end centric assignments. I adapted to Rails on a need to know basis. Fast forward to 2017, and I finally found the time to properly digest each of the official Rails 5.0 guides.
Wanting to be indoctrinated with the ‘Rails 5 way’ of doing things, I initially started working with with a stock
rails new myapp instance.
But after researching Docker Compose Rails setups, I came across Nick Janetakis’ blog post on Docker Compose & Rails. He created an ‘opinionated Rails app template’ generator called orats that made a few minimal customizations to the base Rails application (i.e. adds Bootstrap, Font Awesome, Postgres, a few helpers & view template mods). As an added bonus, it provided a well commented starting point for customizing the Docker Compose setup. I was already starting to add those gems anyways, so I took a shortcut and migrated into a new orats instance.
Also looked at the thoughtbot/suspenders Rails template — but their modifications were a bit too divergent from an ‘out-of-the-box’ Rails setup for my needs.
Chose to configure database as PostgreSQL since that seems to be a hardened standard, I’ve worked with it before, and is easy to setup on Heroku.
Once an admin logs in, they are able to manage the users.
Admin::UserControllerand associated views handle user list/add/view/edit/delete actions.
- Views & forms to are styled with Bootstrap FTW
Usermodel implements basic validations like password length and permissible username values
Admin::UserFormBuildercustom decorates the form field errors
- Model & Selenium acceptance tests assert consistent data & behavior expectations
- A user gets locked out after 3 failed attempts to thwart a theoretical brute force attempt
Considered using Devise, but wanted to fully understand how the authentication process worked. So ‘rolled my own’ with the Rails 5 API.
- Leverages Rails’ ActiveModel
has_secure_passwordfeature which uses Bcrypt. (I was perplexed to learn Bcrypt doesn’t require an external salt).
- Simple cookie stores the session, and expires after two hours.
- Karim’s tutorial on Rails Authentication was insightful.
Quasi-Proxy Served SPA
The demo work portfolio I’m trying to keep private is a static single-page app (SPA) built with Grunt/Node and Angular. It’s simple to serve since it’s entirely static, and I wanted to keep it decoupled from this Rails app since it’s got it’s own build environment and dependencies.
ProxyController will perform a few ‘proxy’ operations to serve the private SPA. A full reverse proxy was more complicated than I wanted to handle—not to mention slow (especially for videos) and resource intensive. So for better or worse, this is how the controller solves this ‘quasi-proxy’ problem:
- On AWS S3 there is a single page Angular app index.html page with embedded JSON data—it is in a private bucket. Authenticate read and render the SPA index page into this controller.
- The rest of the related JS/CSS/util-images are in a public bucket. Inject meta base tag for these assets into the index proxy file.
- The page data has several private image/video assets that point to the private bucket. Parse URLS out of the static JSON, transform into signed URLs that expire just after the session does, inject back into the page.
- Inject logout button into rendered header nav on right, and Google Analytics tags for behavior tracking
Test & CI
Test driven development requires more time to do properly than just manually testing as you go, but I believe it pays dividends later. Well written tests add a degree of confidence when it comes time to extend the feature set, and chase down bugs.
- The model, integration & Selenium acceptance tests run inside of a Docker Compose service.
- The Selenium tests are normally ‘headless’, but with the selenium/standalone-chrome-debug Docker image — can use VNC Viewer to monitor.
- I’ve used RSpec in the past, but Rails 5.0.2 came with MiniTest and DHH seems to prefer it — so decided to give it a try.
- thoughtbot/shoulda-matchers helped make quick work of my
- I considered setting up thoughtbot/capybara-webkit, but fumbled with the install longer than I liked — and the number of open issues on the repo was a little discouraging. So I passed for now.
- On the topic of tests, I found this post insightful on differentiating when to use what type of tests : Three Options for Top-Down Rails Testing
- Am super curious how Rails 5.1’s introduction of Capybara integrated
SystemTestis going to alter the Rails testing landscape & basic CI setups.
Heroku Pipelines handles serving the Staging, Demo and Production instances.
- TravisCI is configured to automatically build & test the master branch and any PRs of the jfroom/jfmk-auth repo.
- Once the build passes, it is automatically deployed to Staging.
dotenvgem ensures environment variable defaults are loaded from a
.envfile, and any variables specific to the Heroku instance will override those.
- Each Heroku instance has it’s own lightweight PostgresSQL database.
- When ready for an official release, the Staging app is promoted to the Production environment with the click of a button in the Heroku GUI (this could also be scripted). This hotswaps the staging application instance ‘slug’ into production.
- Environment variables configure the app to load private portfolio content off S3.
- Emails are sent to admin when a user logs in. A background process sends out the emails asynchronously with
sucker_punch. Utilizing SES as an SMTP service.
- When exceptions are thrown,
exception_notificationwill send out an email to admin. The app is also being monitored by New Relic and PaperTrail.
- App is mapped to a custom subdomain—and the free, domain-validated SSL is dynamically configured with
- One caveat of the Pipelines — running a db migration was a little tricky on a promotion. Had to use a beta ‘release phase’ command. Somehow I missed the log of that event when it happened so I could inspect, but it did show up in PaperTrail.
- A separate Heroku ‘demo’ production instance sets the environment variable
- In demo mode, new users will not be saved, and existing users will not be updated, or deleted (with
ActiveRecord::Rollback). Users will also not be locked out after repeat fails. This is done in order to keep the users active for future visitors to demo, and to prevent system abuse.
- Once logged in, the proxy content is a public demo version of the portfolio.
- The demo instance also runs on a ‘free’ dyno server so it is probably sleeping, and may be a little sluggish starting up.
- Try it out here: https://jfmk-auth-demo.herokuapp.com with credentials
- JFMK-Auth: https://github.com/jfroom/jfmk-auth