00:00:17.520
All right, what I'm talking about today is service-oriented authentication. This is how you authenticate who a user is when you want to start working with service-oriented architecture (SOA). You need to be able to authenticate the same person from service to service.
00:00:30.720
I nearly went with this as a title slide but decided that I would stick with the other ones so that everyone knew they were in the right place. I also think this picture serves as a good visual metaphor for what happens with our systems. Sometimes we start with a nice, shiny feature unique to our app, and then we begin to add more and more components, building it out. Some of the features we develop are very well architected, while others may grow in unexpected ways without a clear understanding of how they evolved. But if it works, we often leave it be and just keep going. Just as planets form through gravity attracting matter into a sphere, the gravitational pull of login systems can often lead to the formation of monoliths.
00:01:18.240
If you were here a couple of sessions ago when Stefan was discussing those large user classes, you know that this happens because once your login system is established, users can log in, and you want them to own certain features. It becomes natural that every new feature gets tied to where users can log in. If you can figure out how to break free from that cycle, it becomes much easier to begin dividing your systems and features into vertical slices of functionality.
00:01:41.439
I have a subtitle for this talk, which is 'Stumbling Towards SOA: The Story of Cloud HDR.' I like this subtitle because SOA isn't a specification. You can't simply read the RFC for it, implement it, and call it done. SOA is a way of thinking about how to architect your applications and design systems that interact with one another. What I can share with you is what I've done and what has worked for me. Whether these approaches will work for you or not is up to you to determine.
00:02:21.840
My original app looked like a big monolithic structure. It began with just the core functionalities for HDR photo processing, but I kept adding more features until I had a massive app that was slow to run, slow to deploy, and slow to test. It was not much fun to work with as a developer because I spent more time waiting on the computer than coding. Recently, I had the opportunity to totally rewrite it, and I created a system that looks more like this: a collection of services that communicate with each other only when needed. Each service can be deployed independently, speeding up deployment and improving performance.
00:03:00.319
The key to this transformation was overcoming the challenge of handling user sessions once logged in. I wanted something akin to what you see at Amazon or Google. In these environments, no matter which web property you log into, you end up at the same login page and are directed back to your original destination. I aimed to replicate that model, so I turned to OAuth 2.
00:03:36.400
OAuth 2 is an open standard for delegating authorization across applications. I am sure you may think, 'This is great if you can do a total rewrite or if you are starting with a fresh app, but what about my monolith?' That’s fine; we can take incremental steps toward improvement. We will look at a way to start from where you are, even if it's a monolith, by adding OAuth 2 provider functionality to it. This will allow your existing app to have its authentication needs delegated, while also enabling you to plug in other services that can work alongside it.
00:04:24.320
To illustrate this, I thought we could refer to it as MUXOA, which stands for Monolith-Centric Service-Oriented Architecture. Of course, I am not affiliated with McDonald's in any way. The talk consists of two main portions: an introduction to OAuth and how it works, and a discussion on how to implement it. Naturally, this wouldn’t be a valid tech talk without starting from zero, as it’s crucial to understand the context, goals, and requirements that led me to make certain decisions.
00:05:07.039
First, a bit of context about me: My name is Jeremy Green, and I’m one of the organizers of the OKC Ruby group. That group was instrumental in preparing me for this talk; they allowed me to preview it and provided valuable feedback. I appreciate their support. You can contact me on Twitter and GitHub; my handle is @JagTheDrummer, or email me at octolabs, and you can also find my website. Now, regarding the app Cloud HDR; it is an HDR photo processing automation web application. It takes photos that might not look very good—one may be overexposed and another underexposed—and transforms them into something visually appealing.
00:06:07.199
Goals and requirements are crucial to understand because they inform your choices. If your goals and requirements differ from mine, your decisions might need to diverge as well. My goal was to have small, focused applications that were easy to test, easy to run, and quick to deploy. I wanted a single sign-on and single sign-off mechanism across various services, aiming to minimize code duplication. Specifically, I didn’t want to build login screens for each app and definitely didn’t want users to have to log in multiple times. Additionally, I wanted to support various service types, including vanilla Rails apps, Ember apps talking to Rails JSON APIs, and one hybrid app that merged aspects of both Rails and Ember.
00:07:11.039
OAuth 2 is essential to the core of this talk. If you have ever encountered 'Log in with Facebook,' 'Log in with Twitter,' or 'Log in with GitHub,' then you have encountered OAuth 2. If you have ever implemented any of these systems, then you’re already halfway to understanding OAuth 2 as I will describe it today. Here’s a brief overview or perhaps a review of how OAuth 2 functions.
00:07:56.559
Consider this simplified version of an OAuth request sequence. Let’s say you have a web client—just a browser—trying to access a protected page on a Rails app. The Rails app will recognize that the user isn’t logged in, and it won’t know who they are, so it cannot allow access to that resource. Therefore, it issues a redirect to an OAuth provider, asking, 'Hey, can you authenticate this user and tell me who they are so I can decide whether to grant access?'. The browser then follows that redirect to the provider. If the user is already logged in there, the provider will recognize who they are. The provider will then redirect back to the original application the browser came from, and again, upon reaching the consumer of the OAuth services, it will post back to the provider, indicating that someone authenticated has arrived with a token. The provider will confirm and send back a token that can be used repeatedly for fetching information on behalf of that user.
00:09:57.440
The consumer then utilizes that token to request additional user information, like their username and email address. The provider will respond with a JSON payload containing that user's details, and finally, the consumer redirects back to the originally requested protected route. Did anyone count how many request and response pairs we went through? Seven! That’s the simplified version—I left out a couple of less informative redirects.
00:10:10.240
While it may seem tedious to implement such a sequence, we have Ruby gems that simplify the process. For instance, the OAuth provider implementation gem is called Doorkeeper. If you attended the refactoring workshop yesterday, you might know that Tutei is the new maintainer of that gem. For the consumer implementation, there’s a gem from Intridea called OmniAuth that handles most of the consumer tasks. The plan is to establish an OAuth 2 provider, which could be an addition to your monolith. Next, create a small gem, in this example, dubbed 'SoOff.' When developing your project, feel free to name your gem something relevant, like 'MyCompAuth' or 'AcmeAuth.' We will use that gem to integrate it into a Rails app to delegate authentication to the provider. Once that is achieved, creating additional services that follow suit becomes quite straightforward.
00:11:06.640
Here’s a quick demo to show you what this looks like. In this example, I have a client app, and I urgently need to attend the design for developers talk tomorrow. The client contains links that redirect back to the provider or the client. Here on the client, when I click a link to access private content, it performs a few redirects before arriving at the provider’s sign-in page. After I sign in, it will redirect back to where I initially intended to go. Hopefully, this goes smoothly... There we go! I’m able to see the private content. If I return to the provider and log out, then go back to the client and try accessing the private content again, it prompts me to sign in. This illustrates the flow.
00:12:48.240
To wrap up this demonstration, all the relevant links are available on my site. The top link leads to a page containing all other links and a link to the presentation slides. Three repositories are involved in this implementation: one for a demo provider, one for the SoOff gem itself, and another for the SoOff client demo. The SoOff gem connects the provider and the consumer. There are also demo links for live hosting on Heroku, allowing you to see it in action.
00:13:31.799
Because implementing everything simultaneously as both provider and consumer can be challenging to follow, we will take a step-by-step approach through the request sequence we viewed earlier. We will start with the first request cycle, which is when the client attempts to access protected content and therefore needs to be redirected to the provider. This begins with the consumer's implementation, which involves entering the OAuth dance.
00:14:54.080
As mentioned, we want to accomplish this in a standalone gem that can be integrated into other projects, thus avoiding the need to recreate similar functionality for every new service we develop. To do this, we’ll start a new Rails plugin. I opted for this approach because I don’t want to deal with mounting routes across all my new services—I want routes to always be available and function as expected across all these services. If you're building something for external developers, you may want to consider a different approach. We need to update the gemspec to include OmniAuth and the OmniAuth OAuth 2 gem for managing tasks efficiently.
00:16:19.120
Next, we create a strategy for OmniAuth. A strategy is a piece of code that informs OmniAuth where to find your provider and how to process the information returned from it. For this initial part, we simply tell it where the provider is located and how to find it. This involves specifying the strategy's name and setting client options, including the site's URL and relevant endpoints for user authorization and token retrieval. Then, we register this strategy with OmniAuth in an initializer. Ideally, you would want to create a generator for this gem, so any new project would have a straightforward initializer setup.
00:17:45.680
To support our dummy application for testing, we will add a public and a private action using a Rails controller generator. Any user can access the public action, while the private action needs user authentication. To achieve this, we will have to define a 'login required' method as a before filter but we won’t build it within the dummy app; instead, it will be created in the gem. Therefore, we need to instruct the dummy app to inherit from our gem’s application controller.
00:18:43.440
Within the gem, we can define the 'login required' method. Initially, we will assume no one is authorized, redirecting users to the OAuth path, which will initiate the OAuth dance with the provider. Once set up, OmniAuth will route the request to the appropriate provider based on the specified URL and endpoints. As a next step, we will look at the provider implementation now, which involves using the Doorkeeper gem, well-known for its ease of use. Adhere closely to the instructions in the README: add the gem to your gem file, run bundle install, and generate the Doorkeeper initializer.
00:20:01.440
After creating the necessary migration for Doorkeeper to maintain essential records, you’ll migrate the database. The configuration will have two significant elements of interest: one is the 'resource owner authenticator,' which is tasked with either returning a currently logged-in user or directing them to a login page. Assuming we are adding this to a device application based on Warden, we will configure it to authenticate with the user scope. This scope could be different if your user model variant does not use 'user' as its name. Furthermore, Doorkeeper provides an application authorization screen, similar to the consent screen you encounter, for example, when logging into GitHub for the first time.
00:21:11.040
For an internal provider that only interacts with internal services, you can skip this screen to enhance user experience. To do this in Doorkeeper, you'll implement the 'skip_authorization' block and return true. You can apply custom logic to determine when to skip authorization if required, especially for public applications. This code executes in your application's context, allowing for flexible authorization handling based on specific business logic.
00:22:19.680
Doorkeeper will manage the request to the authorization endpoint and will redirect back to the consumer app once it has identified a valid user. Browsers excel at following redirects, so it returns to the consumer, and OmniAuth handles posting to the provider to fetch the access token. Doorkeeper then provides this token to the consumer, allowing it to request further information. However, any application-dependent information must be manually implemented, as Doorkeeper does not know which attributes of your user model you might wish to share.
00:23:15.680
To implement this, you will establish a route for OAuth Me and specify the appropriate controller. The action within that controller could be as straightforward as responding with the current resource owner—which is the authenticated user. In the consumer controller, we can ensure that any requests coming in must be authorized by Doorkeeper by placing 'doorkeeper_for' at the top to set up the necessary token variable.
00:23:59.840
Subsequently, we will query for the correct user using the token passed in the request. After successfully retrieving the user, the payload must be converted to JSON for transferring information back through the wire. Now we have a JSON payload originating from the provider that is delivered to the consumer. In the consumer, we must establish a session confirming a valid user login. This required logic is application-specific, so we need to write this ourselves. However, we can encapsulate this process within the SoOff gem, thus reusing it across our services without redundant implementations.
00:25:03.520
Back to the consumer implementation, there’s a need for a method that will intercept raw user information from the provider. We will utilize the access token that OmniAuth provides, appending it to retrieve 'oauth/me'. At this stage, OmniAuth will automatically handle the token inclusion, allowing the provider to identify the user. Additionally, we need to specify where the user ID can be found in the JSON payload. OmniAuth requires two hashes of information: 'info' and an 'extra' hash containing the entire payload for potential further utilization.
00:26:29.440
Finally, to conclude the OAuth flow, implement two callbacks that OmniAuth uses to exit the dance. If OmniAuth successfully retrieves user data, it redirects to 'oauth/so/callback'. If it fails to identify the user, the fallback route ‘oauth/failure’ is called. Define these two routes and ensure they point to the correct controller. In the create method, we will leverage environment variables set up by OmniAuth to retrieve and possibly create a user based on the ID pulled from the JSON payload.
00:27:17.360
Once we create or find the user, we will update their email, bio, or any changing information included in the payload, ensuring up-to-date details with each new authorization. To finalize the process, we will store the user ID in the session, indicating that a valid user is logged in. Lastly, we redirect the user to `omniuth.origin`, redirecting them back to the original location they wanted to go to. The failure route should handle showing a message based on the notice provided in the auth request.
00:28:39.280
We are nearing the end of this implementation. The main app can add additional helper methods that facilitate managing users authenticated via the provider. For example, we can add a `current_user` method, retrieving the user ID from the session, and a `signed_in?` method to quickly check for a current user. By registering these methods as helpers, relatable naming conventions based on the Devise framework help maintain consistency in dealing with user sessions in both provider and consumer applications.
00:29:18.600
As we incorporate all these elements, it’s crucial to handle single sign-off effectively. You wouldn’t want users to log off from your consumer without also logging off from the provider, or the reverse. To implement this effectively, have the provider set a cookie to track the signed-in user. When users log out, we can use a method to remove that cookie, enabling the consumer to detect the logout event and reset its session. This part is essential for multi-consumer environments, where logging out of one app should clear the user sessions across all contexts.
00:30:34.880
On the provider side, the approach is straightforward: establish a mechanism in the Devise routes to handle custom session controllers. Within both sessions, create and destroy methods, you can call 'super' and pass a block for additional functionality. For the create method, allow Devise to establish the user session, but just before redirecting, set the user cookie to facilitate sign-offs. The destroy method should similarly ensure removal of the user cookie before redirecting out after ending that session.
00:31:27.680
On the consumer side of the implementation, introduce a 'check cookie' before filter, which resets the session unless there’s a valid cookie indicating the current user is logged in. Check for three conditions: the existence of a cookie named SoOff, a session user ID, and whether they match. If any of these conditions are unsatisfied, it indicates a need to reset the session, as it implies the user has logged out or has switched accounts.
00:32:08.480
Lastly, we need to create a logout route in the consumer app that resets the session, followed by redirecting to the provider's logout route. This approach ensures the cookie is removed, and the consumer acknowledges the lack of a valid session. This process is crucial for environments with multiple consumer applications, as logging out from one should log the user out of all.
00:33:45.680
Now that we have a functional gem, we can utilize it in new Rails applications to establish services. Start by creating a new Rails app designated for the SoOff consumer and, within the gem file, reference the new SoOff gem. For development, you can point to the local path; during production, vendorizing the gem will allow you to pull the latest version as needed. Overall, the implementation will unfold as you set up your OAuth applications within the Doorkeeper area and register new consumer applications.
00:35:12.240
To conclude, setting up a provider and building a convenience gem to facilitate additional services is an excellent way to start your journey into SOA. The main takeaway is that you can kick off from where you are today—regardless of the state of your monolith. By plugging in provider functionality, you can incrementally evolve toward a service-oriented architecture. Thank you for watching.