diff --git a/docs/Access-Control-Lists.md b/docs/Access-Control-Lists.md new file mode 100644 index 0000000..f201041 --- /dev/null +++ b/docs/Access-Control-Lists.md @@ -0,0 +1,43 @@ +Access Control Lists (ACLs) are the main tool Rails::Auth provides for AuthZ. ACLs use a set of route-by-route [[matchers]] to control access to particular resources. + +Rails::Auth encourages the use of YAML files for storing ACL definitions, although the use of YAML is not mandatory and the corresponding object structure output from `YAML.load` can be passed in instead. The following is an example of an ACL definition in YAML: + +```yaml +--- +- resources: + - method: ALL + path: /foo/bar/.* + allow_x509_subject: + ou: ponycopter + allow_claims: + groups: ["example"] +- resources: + - method: ALL + path: /_admin/?.* + allow_claims: + groups: ["admins"] +- resources: + - method: GET + path: /internal/frobnobs/.* + allow_x509_subject: + ou: frobnobber +- resources: + - method: GET + path: / + allow_all: true +``` + +An ACL consists of a list of guard expressions, each of which contains a list of resources and a set of [[matchers]] which can authorize access to those resources. Access will be authorized if *any* of the matchers for a given resource are a match (i.e. matchers have "or"-like behavior, not "and"-like behavior). Requiring more than one credential to access a resource is not supported directly, but can be accomplished by having credential-extracting middleware check for credentials from previous middleware before adding new credentials to the Rack environment. + +Resources are defined by the following constraints: + +* **method**: The requested HTTP method, or `"ALL"` to allow any method +* **path**: A regular expression to match the path. `\A` and `\z` are added by default to the beginning and end of the regex to ensure the entire path and not a substring is matched. +* **host** (optional): a regular expression to match the `Host:` header passed by the client. Useful if your app services traffic for more than one hostname and you'd like to restrict ACLs by host. + +The following [[matchers]] are built-in and always available: + +* **allow_all**: (options: `true` or `false`) always allow requests to the + given resources (so long as `true` is passed as the option) + +Rails::Auth also ships with [[matchers]] for [[X.509]] certificates. \ No newline at end of file diff --git a/docs/Comparison-With-Other-Libraries.md b/docs/Comparison-With-Other-Libraries.md new file mode 100644 index 0000000..12844c1 --- /dev/null +++ b/docs/Comparison-With-Other-Libraries.md @@ -0,0 +1,29 @@ +Rails::Auth was primarily intended for use in environments with the following: + +* [Microservices]: Rails::Auth is primarily intended to support environments with many services written as Rails apps which need to make authenticated requests to each other. Square uses Rails::Auth in an environment where we have many Rails microservices (and microservices written in other languages) authenticating to each other with [[X.509]] certificates. +* [Claims-Based Identity]: Rails::Auth is designed to work in conjunction with a central [Single Sign-On] (SSO) system which issues credentials that provide user identities. Rails::Auth does not ship with a specific implementation of an SSO system, but makes it easy to integrate with existing ones. + +Below is a comparison of how Rails::Auth relates to the existing landscape of Rails AuthN and AuthZ libraries. These are grouped into two different categories: libraries Rails::Auth replaces, and libraries with which +Rails::Auth can be used in a complementary fashion. + +## Replaces: + +* [Warden]: Uses a single "opinionated" Rack middleware providing user-centric authentication and methods that allow controllers to imperatively interrogate the authentication context for authorization purposes. By comparison Rails::Auth is not prescriptive and much more flexible about credential types (supporting credentials for both user and service clients) and uses declarative authorization policies in the form of ACLs. + +* [Devise]: A mature, flexible, expansive framework primarily intended for user authentication. Some of the same caveats as Warden apply, however Devise provides a framework for modeling users within a Rails app along with common authentication flows, making it somewhat orthogonal to what Rails::Auth provides. Rails::Auth is designed to easily support [claims-based identity] systems where user identity is outsourced to a separate microservice. + +## Complements: + +* [Pundit]: Domain object-centric fine-grained authorization using clean object-oriented APIs. Pundit makes authorization decisions around particular objects based on policy objects and contexts. Rails::Auth's credentials can be used as a powerful policy context for Pundit. + +* [CanCanCan]: a continuation of the popular CanCan AuthZ library after a period of neglect. Uses a more DSL-like approach to AuthZ than Pundit, but provides many facilities similar to Pundit for domain object-centric + AuthZ. + +[Warden]: https://github.com/hassox/warden/wiki +[Devise]: https://github.com/plataformatec/devise +[Pundit]: https://github.com/elabs/pundit +[CanCanCan]: https://github.com/CanCanCommunity/cancancan + +[microservices]: http://martinfowler.com/articles/microservices.html +[claims-based identity]: https://en.wikipedia.org/wiki/Claims-based_identity +[single sign-on]: https://en.wikipedia.org/wiki/Single_sign-on \ No newline at end of file diff --git a/docs/Design-Overview.md b/docs/Design-Overview.md new file mode 100644 index 0000000..9b38f02 --- /dev/null +++ b/docs/Design-Overview.md @@ -0,0 +1,37 @@ +Rails::Auth makes use of multiple, independent, single-purpose middleware +classes to handle specific types of AuthN/AuthZ. + +## AuthN + +Rails::Auth ships with the following AuthN middleware: + +* `Rails::Auth::X509::Middleware`: authenticates [[X.509]] certificates obtained + from the Rack environment. + +The goal of Rails::Auth's AuthN middleware is to authenticate *credentials* +taken from the Rack environment and place objects representing them under +the `"rails-auth.credentials"` key within the Rack environment for use by +subsequent AuthN or AuthZ middleware. The built-in support is for X.509 +client certificates, but other middleware could handle authentication of +cookies or (OAuth) bearer credentials. + +The intended usage is to have multiple AuthN middlewares that are capable +of extracting different types of credentials, but also allowing AuthZ +middleware to apply a single policy to all of them. It's also possible to +chain AuthN middleware together such that one credential obtained earlier +in the middleware stack is used to authenticate another (for e.g. +[channel-bound cookies]). + +[channel-bound cookies]: http://www.browserauth.net/channel-bound-cookies + +## AuthZ + +Rails::Auth ships with one primary AuthZ middleware: + +* `Rails::Auth::ACL::Middleware`: support for [[Access Control Lists]] (ACLs). + +ACLs let you write a single, declarative policy for authorization in your application. ACLs are pluggable and let you write a single policy which can authorize access using different types of credentials. + +ACLs are a declarative approach to authorization, consolidating policies into a single file that can be easily audited by a security team without deep understanding of the many eccentricities of Rails. These policies +provide coarse-grained authorization based on routes (as matched by regexes) and the credentials extracted by the AuthN middleware. However, the do not provide AuthZ which includes specific domain objects, or +policies around them. For that we suggest using a library like [Pundit](https://github.com/elabs/pundit). \ No newline at end of file diff --git a/docs/Error-Handling.md b/docs/Error-Handling.md new file mode 100644 index 0000000..525f52c --- /dev/null +++ b/docs/Error-Handling.md @@ -0,0 +1,15 @@ +Rails::Auth includes two different middlewares for rendering error responses: one for debugging, and one intended for production environments which renders a static 403 page (or corresponding JSON response). + +## Debug Page + +The `Rails::Auth::ErrorPage::DebugMiddleware` provides a rich inspector for why access was denied: + +![Debug Page](https://github.com/square/rails-auth/blob/master/images/debug_error_page.png?raw=true) + +This page is enabled automatically in the development environment for Rails apps. It can also be enabled in production by passing the `error_page: :debug` option to `Rails::Auth::ConfigBuilder.production`. See [[Rails Usage]] for more information. + +## Static Page + +The `Rails::Auth::ErrorPage::Middleware` renders a static HTML page and/or JSON response in the event of an authorization failure. + +This middleware is used by default in the production environment, and defaults to rendering `public/403.html`. The location of the page can be overridden using the `error_page:` option and passing a `Pathname` or `String` to the file's location. ERB is not presently supported. See [[Rails Usage]] for more information. diff --git a/docs/Home.md b/docs/Home.md new file mode 100644 index 0000000..6b7639f --- /dev/null +++ b/docs/Home.md @@ -0,0 +1,15 @@ +## Intro + +* [[Design Overview]]: an overview of Rails::Auth's middleware-based design +* [[Comparison With Other Libraries]]: How Rails::Auth compares to other Rails/Rack auth libraries/frameworks + +## Usage + +* [[Rails Usage]]: how to add Rails::Auth to a Rails app +* [[Rack Usage]]: how to use Rails::Auth's middleware outside of a Rails app +* [[Access Control Lists]]: how to define policy for what actions are allowed +* [[Matchers]]: how to make access control decisions based on credentials +* [[X.509]]: how to authorize requests using X.509 client certificates +* [[Error Handling]]: show a rich debugger or static 403 page on authorization errors +* [[Monitor]]: invokes a user-specified callback each time an AuthZ decision is made +* [[RSpec Support]]: use RSpec to write integration tests for Rails::Auth features and specs for ACLs \ No newline at end of file diff --git a/docs/Matchers.md b/docs/Matchers.md new file mode 100644 index 0000000..4afe3d6 --- /dev/null +++ b/docs/Matchers.md @@ -0,0 +1,28 @@ +Matchers are the component of Rails::Auth that make authorization decisions based on credentials. The following matchers are built-in and always available: + +The following [[matchers]] are built-in and always available: + +* **allow_all**: (options: `true` or `false`) always allow requests to the + given resources (so long as `true` is passed as the option) + +The `Rails::Auth::X509::Matcher` class can be used to make authorization decisions based on X.509 certificates. For more information, see the [[X.509]] Wiki page. + +Custom [[matchers]] can be any Ruby class that responds to the `#match` method. The full Rack environment is passed to `#match`. The corresponding object from the ACL definition is passed to the class's `#initialize` method. + +Here is an example of a simple custom matcher: + +```ruby +class MyClaimsMatcher + def initialize(options) + @options = options + end + + def match(env) + claims = Rails::Auth.credentials(env)["claims"] + return false unless credential + + @options["groups"].any? { |group| claims["groups"].include?(group) } + end +end + +``` \ No newline at end of file diff --git a/docs/Monitor.md b/docs/Monitor.md new file mode 100644 index 0000000..ceb1045 --- /dev/null +++ b/docs/Monitor.md @@ -0,0 +1,18 @@ +The `Rails::Auth::Monitor::Middleware` invokes a user-specified callback each time an AuthZ decision is made. The callback should look like this: + +```ruby +my_monitor_callback = lambda do |env, success| + [...] +end +``` + +The parameters are: + +* **env:** the full Rack environment associated with the request +* **success:** whether or not the request was authorized + +On Rails, you can pass this callback as the `monitor:` option to `Rails::Auth::ConfigBuilder.production`. See [[Rails Usage]] for more information. + +On Rack, you will have to instantiate the middleware yourself. See [[Rack Usage]] for more information. + +These callbacks are useful for logging authorization decisions and/or reporting authorization failures to e.g. a central monitoring/alerting system. \ No newline at end of file diff --git a/docs/RSpec-Support.md b/docs/RSpec-Support.md new file mode 100644 index 0000000..5aa6a5f --- /dev/null +++ b/docs/RSpec-Support.md @@ -0,0 +1,117 @@ +Rails::Auth includes RSpec support useful for writing Rails integration tests and/or spec for your ACLs to ensure they have the behavior you expect. + +To enable RSpec support, require the following: + +```ruby +require "rails/auth/rspec" +``` + +## Helper Methods + +### `with_credentials`: simulate credentials in tests + +Configures a `Hash` of credentials (or doubles of them) to be used in the test. Helpful for simulating various scenarios in integration tests: + +```ruby +RSpec.describe MyApiController, type: :request do + describe "#index" do + let(:example_app) { "foobar" } + let(:another_app) { "quux" } + + it "permits access to the 'foobar' app" do + with_credentials(x509: x509_certificate(cn: example_app)) do + get my_api_path + end + + expect(response.code).to eq "200" + end + + it "disallows the 'quux' app" do + with_credentials(x509: x509_certificate(cn: another_app)) do + get my_api_path + end + + expect(response.code).to eq "403" + end + end +end +``` + +### `x509_certificate`, `x509_certificate_hash`: create X.509 certificate doubles + +The `x509_certificate` method creates a [verifying double] of a `Rails::Auth::X509::Certificate`. See the `#with_credentials` example for use in context. + +It accepts the following options: + +* **cn**: common name of the certificate (e.g. app name or app/host combo) +* **ou**: organizational unit of the certificate (e.g. app name, team name) + +The `x509_certificate_hash` method produces a credential hash containing a `Rails::Auth::X509::Certificate`, and is shorthand so you don't have to do `{"x509" => x509_certificate(...)}`. Below is the same example as from `#with_credentials`, but rewritten with the `x509_certificate_hash` shorthand: + +```ruby +RSpec.describe MyApiController, type: :request do + describe "#index" do + let(:example_app) { "foobar" } + let(:another_app) { "quux" } + + it "permits access to the 'foobar' app" do + with_credentials(x509_certificate_hash(cn: example_app)) do + get my_api_path + end + + expect(response.code).to eq "200" + end + + it "disallows the 'quux' app" do + with_credentials(x509_certificate_hash(cn: another_app)) do + get my_api_path + end + + expect(response.code).to eq "403" + end + end +end +``` + +See also: [[X.509]] Wiki page. + +[verifying double]: https://relishapp.com/rspec/rspec-mocks/docs/verifying-doubles + +## ACL Specs + +Rails::Auth provides its own extensions to RSpec to allow you to write specs for the behavior of ACLs. + +Below is an example of how to write an ACL spec: + +```ruby +RSpec.describe "example_acl.yml", acl_spec: true do + let(:example_credentials) { x509_certificate_hash(ou: "ponycopter") } + + subject do + Rails::Auth::ACL.from_yaml( + File.read("/path/to/example_acl.yml"), + matchers: { allow_x509_subject: Rails::Auth::X509::Matcher } + ) + end + + describe "/path/to/resource" do + it { is_expected.to permit get_request(credentials: example_credentials) } + it { is_expected.not_to permit get_request } + end +end +``` + +* Request builders: The following methods build requests from the described path: + * `get_request` + * `head_request` + * `put_request` + * `post_request` + * `delete_request` + * `options_request` + * `path_request` + * `link_request` + * `unlink_request` + +The following matchers are available: + +* `allow_request`: allows a request with the given Rack environment, and optional credentials \ No newline at end of file diff --git a/docs/Rack-Usage.md b/docs/Rack-Usage.md new file mode 100644 index 0000000..bf572ff --- /dev/null +++ b/docs/Rack-Usage.md @@ -0,0 +1,152 @@ +Rails::Auth, despite the name, includes a Rack-only mode which is not dependent on Rails: + +```ruby +require "rails/auth/rack" +``` + +To use Rails::Auth you will need to configure the relevant AuthN and AuthZ middleware for your app. + +Rails::Auth ships with the following middleware: + +* **AuthN**: `Rails::Auth::X509::Middleware`: support for authenticating clients by their SSL/TLS client certificates. Please see [[X.509]] for more information. +* **AuthZ**: `Rails::Auth::ACL::Middleware`: support for authorizing requests using [[Access Control Lists]] (ACLs). + +## ACL Middleware + +Once you've defined an [[Access Control List|Access Control Lists]], you'll need to create a corresponding ACL object in Ruby and a middleware to authorize requests using that ACL. Add the following code anywhere you can modify the middleware chain (e.g. config.ru): + +```ruby +app = MyRackApp.new + +acl = Rails::Auth::ACL.from_yaml( + File.read("/path/to/my/acl.yaml"), + matchers: { allow_claims: MyClaimsMatcher } +) + +acl_auth = Rails::Auth::ACL::Middleware.new(app, acl: acl) + +run acl_auth +``` + +You'll need to pass in a hash of predicate matchers that correspond to the keys in the ACL. + +## X.509 Middleware + +Add an `Rails::Auth::X509::Middleware` object to your Rack middleware chain to verify [[X.509]] client certificates (in e.g. config.ru): + +```ruby +app = MyRackApp.new + +acl = Rails::Auth::ACL.from_yaml( + File.read("/path/to/my/acl.yaml") + matchers: { allow_x509_subject: Rails::Auth::X509::Matcher } +) + +acl_auth = Rails::Auth::ACL::Middleware.new(app, acl: acl) + +x509_auth = Rails::Auth::X509::Middleware.new( + acl_auth, + ca_file: "/path/to/my/cabundle.pem" + cert_filters: { "X-SSL-Client-Cert" => :pem }, + require_cert: true +) + +run x509_auth +``` + +The constructor takes the following parameters: + +* **app**: the next Rack middleware in the chain. You'll likely want to use an `Rails::Auth::ACL::Middleware` instance as the next middleware in the chain. +* **ca_file**: Path to the certificate authority (CA) bundle with which to authenticate clients. This will typically be the certificates for the internal CA(s) you use to issue X.509 certificates to internal services, as opposed to commercial CAs typically used by browsers. Client certificates will be ignored unless they can be verified by one of the CAs in this bundle. +* **cert_filters**: A `Hash` which configures how client certificates are extracted from the Rack environment. You will need to configure your web server to include the certificate in the Rack environment. See notes below for more details. +* **require_cert**: (default `false`) require a valid client cert in order for the request to complete. This disallows access to your app from any clients who do not have a valid client certificate. When enabled, the middleware will raise the `Rails::Auth::X509::CertificateVerifyFailed` exception. + +When creating `Rails::Auth::ACL::Middleware`, make sure to pass in `matchers: { allow_x509_subject: Rails::Auth::X509::Matcher }` in order to use this predicate in your ACLs. This predicate matcher is not enabled by default. + +For client certs to work, you will need to configure your web server to include them in your Rack environment, and also configure `cert_filters` correctly to filter and process them from the Rack environment. For more information on configuring `cert_filters`, see the [[X.509]] Wiki page. + +## Error Page Middleware + +When an authorization error occurs, the `Rails::Auth::NotAuthorizedError` exception is raised up the middleware chain. However, it's likely you would prefer to show an error page than have an unhandled exception. + +You can write your own middleware that catches `Rails::Auth::NotAuthorizedError` if you'd like. However, this library includes two middleware for rescuing this exception for you and displaying an error page. + +For more information, see the [[Error Handling]] Wiki page. + +#### Rails::Auth::ErrorPage::DebugMiddleware + +This middleware displays a detailed error page intended to help debug authorization errors. Please be aware this middleware leaks information about your ACL to a potential attacker. Make sure you're ok with that information being public before using it. If you would like to avoid leaking that information, see `Rails::Auth::ErrorPage::Middleware` below. + +```ruby +app = MyRackApp.new + +acl = Rails::Auth::ACL.from_yaml( + File.read("/path/to/my/acl.yaml") + matchers: { allow_x509_subject: Rails::Auth::X509::Matcher } +) + +acl_auth = Rails::Auth::ACL::Middleware.new(app, acl: acl) + +x509_auth = Rails::Auth::X509::Middleware.new( + acl_auth, + ca_file: "/path/to/my/cabundle.pem" + cert_filters: { "X-SSL-Client-Cert" => :pem }, + require_cert: true +) + +error_page = Rails::Auth::ErrorPage::DebugMiddleware.new(x509_auth, acl: acl) + +run error_page +``` + +#### Rails::Auth::ErrorPage::Middleware + +This middleware catches `Rails::Auth::NotAuthorizedError` and renders a given static HTML file, e.g. the 403.html file which ships with Rails. It will not give detailed errors to your users, but it also won't leak information to an attacker. + +```ruby +app = MyRackApp.new + +acl = Rails::Auth::ACL.from_yaml( + File.read("/path/to/my/acl.yaml") + matchers: { allow_x509_subject: Rails::Auth::X509::Matcher } +) + +acl_auth = Rails::Auth::ACL::Middleware.new(app, acl: acl) + +x509_auth = Rails::Auth::X509::Middleware.new( + acl_auth, + ca_file: "/path/to/my/cabundle.pem" + cert_filters: { "X-SSL-Client-Cert" => :pem }, + require_cert: true +) + +error_page = Rails::Auth::ErrorPage::Middleware.new( + x509_auth, + page_body: File.read("path/to/403.html") +) + +run error_page +``` + +## Monitor Middleware + +`Rails::Auth::Monitor::Middleware` allows you to configure a user-specified callback which is fired each time an authorization decision is made: + +```ruby +app = MyRackApp.new + +acl = Rails::Auth::ACL.from_yaml( + File.read("/path/to/my/acl.yaml"), + matchers: { allow_claims: MyClaimsMatcher } +) + +acl_auth = Rails::Auth::ACL::Middleware.new(app, acl: acl) + +callback = lambda do |env, success| + puts "AuthZ result for #{env["PATH_INFO"]}: #{success}" +end + +monitor_middleware = Rails::Auth::Monitor::Middleware.new(acl_auth, callback) + +run monitor_middleware +``` \ No newline at end of file diff --git a/docs/Rails-Usage.md b/docs/Rails-Usage.md new file mode 100644 index 0000000..a567e1f --- /dev/null +++ b/docs/Rails-Usage.md @@ -0,0 +1,138 @@ +## Gemfile + +Add the following to your Rails app's Gemfile: + +```ruby +gem "rails-auth" +``` + +Then run: + +``` +$ bundle +``` + +You should see the `rails-auth` gem be added to your app. + +## Configuration + +The `Rails::Auth::ConfigBuilder` module contains methods to configure Rails::Auth for various environments. We'll be adding it to `config/application.rb` and environment-specific configs. We'll also need to create an ACL file: + +### config/acl.yml + +This file contains our app's [[Access Control List|Access Control Lists]]. Here is a starter ACL you can use that allows access to `/` and `/assets`: + +```yaml +--- +- resources: + - path: "/" + method: GET + - path: "/assets/.*" + method: GET + allow_all: true +``` + +Each time you add new routes to your app, you will have to add them to your ACL and decide how they should be authorized. See [[Access Control Lists]] for more information. + +### config/application.rb + +```ruby +module MyApp + class Application < Rails::Application + [...] + + Rails::Auth::ConfigBuilder.application(config, matchers: { allow_x509_subject: Rails::Auth::X509::Matcher }) + end +end +``` + +The `application` method accepts the following options: + +* **acl_file:** path to the [[Access Control List|Access Control Lists]] file for this app. Defaults to `config/acl.yml`. +* **matchers:** a set of ACL [[matchers]] to use. The above example configures [[X.509]] matchers only. + +### config/environments/development.rb + +```ruby +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + Rails::Auth::ConfigBuilder.development(config) +end +``` + +The `development` method accepts the following options: + +* **development_credentials:** a hash of simulated credentials to use in the development environment. +* **error_page:** (defaults to `:debug`) enables [[error page middleware|error handling]] for handling AuthZ failures. + +### config/environments/test.rb + +```ruby +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + Rails::Auth::ConfigBuilder.test(config) +end +``` + +This middleware takes no options, but configures `Rails.configuration.x.rails_auth.test_credentials` to be injected into the request during tests. This is needed for the [[RSpec Support]]'s `with_credentials` helper method to function correctly. + +### config/environments/production.rb + +```ruby +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + Rails::Auth::ConfigBuilder.production( + config, + cert_filters: { "X-SSL-Client-Cert" => :pem }, + ca_file: "/path/to/your/internal/ca.pem" + ) +end +``` + +The `production` method accepts the following options: + +* **cert_filters**: A `Hash` which configures how client certificates are extracted from the Rack environment. You will need to configure your web server to include the certificate in the Rack environment. See notes below for more details. +* **ca_file**: Path to the certificate authority (CA) bundle with which to authenticate clients. This will typically be the certificates for the internal CA(s) you use to issue [[X.509]] certificates to internal services, as opposed to commercial CAs typically used by browsers. Client certificates will be ignored unless they can be verified by one of the CAs in this bundle. +* **require_cert**: (default `false`) require a valid client cert in order for the request to complete. This disallows access to your app from any clients who do not have a valid client certificate. When enabled, the middleware will raise the `Rails::Auth::X509::CertificateVerifyFailed` exception. +* **error_page:** (defaults to `public/403.html`) renders a static [[error page|error handling]] in the event of an authorization failure. Takes a `Pathname` or `String` path to a static file to render, `:debug` to enable a rich access debugger, or `false` to disable the error page and let the exception bubble up. +* **monitor:** a [[monitor]] proc which is called with the Rack environment and whether or not authorization was successful. Useful for logging and/or for reporting AuthZ failures to an internal monitoring system. + +For [[X.509]] client certificate-based authentication to work, you will need to configure your web server to include them in your Rack environment, and also configure `cert_filters` correctly to filter and process them from the Rack environment. Please see the [[X.509]] page for more information on how to configure `cert_filters`. + +## Controller Methods + +Rails::Auth includes a module of helper methods you can use from Rails controllers. Include them like so: + +```ruby +class ApplicationController < ActionController::Base + # Include this in your ApplicationController + include Rails::Auth::ControllerMethods +end +``` + +This defines the following methods: + +* `#credentials`: obtain a `HashWithIndifferentAccess` containing all of the credentials that Rails::Auth has extracted using its AuthN middleware. + +Below is a larger example of how you can use the `credentials` method in your app: + +```ruby +class ApplicationController < ActionController::Base + # Include this in your ApplicationController + include Rails::Auth::ControllerMethods + + def x509_certificate_ou + credentials[:x509].try(:ou) + end + + def current_username + # Note: Rails::Auth doesn't provide a middleware to extract this, it's + # just an example of how you could use it with your own claims-based + # identity system. + credentials[:identity_claims].try(:username) + end +end +``` \ No newline at end of file diff --git a/docs/X.509.md b/docs/X.509.md new file mode 100644 index 0000000..ea5c168 --- /dev/null +++ b/docs/X.509.md @@ -0,0 +1,55 @@ +Rails::Auth is designed to support microservice ecosystems identified by [X.509 Certificates](https://en.wikipedia.org/wiki/X.509). This provides strong cryptographic authentication when used in conjunction with [HTTPS](https://en.wikipedia.org/wiki/HTTPS). To use Rails::Auth in this capacity, you will need to set up an internal [certificate authority](https://en.wikipedia.org/wiki/Certificate_authority) (using e.g. [cfssl](https://github.com/cloudflare/cfssl) or [openssl ca](https://www.openssl.org/docs/manmaster/apps/ca.html)) and then create an X.509 certificate for each microservice in your infrastructure. + +## ACL Matcher + +To enable X.509 support in Rails::Auth, pass in the [[matcher|matchers]] class for X.509 certificates: `Rails::Auth::X509::Matcher` class when creating your [[Access Control List|Access Control Lists]] object, along with a key name you'll use in your ACL like `allow_x509_subject`. + +Now when you define your ACL, you can restrict access to particular routes based on the client's X.509 certificate: + +```yaml +--- +- resources: + - method: ALL + path: /foo/bar/.* + allow_x509_subject: + ou: ponycopter +``` + +The following options can be passed to the matcher: + +* **cn:** common name of the certificate, e.g. app name or app/host +* **ou:** organizational unit name of the certificate, e.g. app name or team name + +## cert_filters + +For [[X.509]] client certificate-based authentication to work, you will need to configure your web server to include them in your Rack environment, and also configure `cert_filters` correctly to filter and process them from the Rack environment. + +For example, if you're using nginx + Passenger, you'll need to add something like the following to your nginx configuration: + +``` +passenger_set_cgi_param X-SSL-Client-Cert $ssl_client_raw_cert; +``` + +Once the client certificate is in the Rack environment in some form, you'll need to configure a filter object which can convert it from its Rack environment form into an `OpenSSL::X509::Certificate` instance. There are +two built in filters you can reference as symbols to do this: + +* `:pem`: parses certificates from the Privacy Enhanced Mail format +* `:java`: converts `sun.security.x509.X509CertImpl` certificate chains + +The `cert_filters` parameter is a mapping of Rack environment names to corresponding filters: + +```ruby +cert_filters: { "X-SSL-Client-Cert" => :pem } +``` + +In addition to these symbols, a filter can be any object that responds to the `#call` method, such as a `Proc`. The following filter will parse PEM certificates: + +```ruby +cert_filters: { + "X-SSL-Client-Cert" => proc do |pem| + OpenSSL::X509::Certificate.new(pem) + end +} +``` + +When certificates are recognized and verified, a `Rails::Auth::X509::Certificate` object will be added to the Rack environment under `env["rails-auth.credentials"]["x509"]`. This middleware will never add any certificate to the environment's credentials that hasn't been verified against the configured CA bundle. \ No newline at end of file