Skip to content

mdsol/mauth-client-ruby

Repository files navigation

MAuth-Client

This gem consists of MAuth::Client, a class to manage the information needed to both sign and authenticate requests and responses, and middlewares for Rack and Faraday which leverage the client's capabilities.

MAuth-Client exists in a variety of languages (.Net, Go, R etc.), see the implementations list for more info.

Installation

Add this line to your application's Gemfile:

gem 'mauth-client'

And then execute:

$ bundle

Or install it yourself as:

$ gem install mauth-client

Configuration

Configuration is set through environment variables:

  • MAUTH_PRIVATE_KEY

    • Required for signing and for authentication.
  • MAUTH_PRIVATE_KEY_FILE

    • May be used instead of MAUTH_PRIVATE_KEY, mauth-client will load the file instead.
  • MAUTH_APP_UUID

    • Required in the same circumstances where a private_key is required.
  • MAUTH_URL

    • Required for authentication but not for signing. Needed to retrieve public keys. Usually this is https://mauth.imedidata.com for production.
  • MAUTH_API_VERSION

    • Required for authentication but not for signing. only v1 exists as of this writing. Defaults to v1.
  • MAUTH_V2_ONLY_SIGN_REQUESTS

    • If true, all outgoing requests will be signed with only the V2 protocol. Defaults to false.
  • MAUTH_V2_ONLY_AUTHENTICATE

    • If true, any incoming request or incoming response that does not use the V2 protocol will be rejected. Defaults to false.
  • MAUTH_DISABLE_FALLBACK_TO_V1_ON_V2_FAILURE

    • If true, any incoming V2 requests that fail authentication will not fall back to V1 authentication. Defaults to false.
  • MAUTH_V1_ONLY_SIGN_REQUESTS

    • If true, all outgoing requests will be signed with only the V1 protocol. Defaults to true. Note, cannot be true if MAUTH_V2_ONLY_SIGN_REQUESTS is also true.
  • MAUTH_USE_RAILS_CACHE

    • If true, Rails.cache is used to cache public keys for authentication.

This is simply loaded and passed to either middleware or directly to a MAuth::Client instance. See the documentation for MAuth::Client#initialize for more details of what it accepts. Usually you will want:

MAUTH_CONF = MAuth::Client.default_config

The .default_config method takes a number of options to tweak its expectations regarding defaults. See the documentation for MAuth::Client.default_config for details.

The private_key and app_uuid are required for signing and for authentication. They’ll only work if the app_uuid has been stored in MAuth with a public key corresponding to the private_key.

The mauth_baseurl and mauth_api_version are required for authentication. These tell the MAuth-Client where and how to communicate with the MAuth service.

The v2_only_sign_requests and v2_only_authenticate flags were added to facilitate conversion from the MAuth V1 protocol to the MAuth V2 protocol. By default both of these flags are false. See Protocol Versions below for more information about the different versions.

v2_only_sign_requests v2_only_authenticate
true requests are signed with only V2 requests and responses are authenticated with only V2
false requests are signed with V1 and V2 requests and responses are authenticated with the highest available protocol version

Generating keys

To generate a private key (mauth_key) and its public counterpart (mauth_key.pub) run:

openssl genrsa -out mauth_key 2048
openssl rsa -in mauth_key -pubout -out mauth_key.pub

Rack Middleware Usage

MAuth-Client provides a middleware for request authentication and response verification in mauth/rack.

require 'mauth/rack'

If you are using other rack middlewares, the MAuth middleware MUST come FIRST in the stack of middlewares. This means it is closest to the HTTP layer, furthest from the application. If any other middlewares which modify the incoming request or outgoing response lie between the HTTP layer and the MAuth middleware, incoming requests will probably fail to authenticate and outgoing response signatures will be invalid (and fail when the requester tries to authenticate them).

Using these middlewares in rails consists of calls to config.middleware.use in the appropriate place (see the Rails Guides for more info).

Using the MAuth::Rack::ResponseSigner middleware is optional, but highly recommended. If used, this should come before the MAuth::Rack::RequestAuthenticator middleware. The ResponseSigner can be used ONLY if you have an app_uuid and private_key specified in your mauth configuration.

config.middleware.use MAuth::Rack::ResponseSigner, MAUTH_CONF

Then request authentication:

config.middleware.use MAuth::Rack::RequestAuthenticator, MAUTH_CONF

However, assuming you have a route /app_status, you probably want to skip request authentication for that. There is a middleware (RequestAuthenticatorNoAppStatus) to make that easier:

config.middleware.use MAuth::Rack::RequestAuthenticatorNoAppStatus, MAUTH_CONF

You may want to configure other conditions in which to bypass MAuth authentication. The middleware takes an option on the :should_authenticate_check key, which is a ruby proc that is passed to the request's rack env and must result in a boolean. If the result is true(ish), the middleware will authenticate the incoming request; if false, it will not. The :should_authenticate_check parameter is OPTIONAL. If omitted, all incoming requests will be authenticated.

Here are a few example :should_authenticate_check procs:

MAUTH_CONF[:should_authenticate_check] = proc do |env|
  env['REQUEST_METHOD'] == 'GET'
end
config.middleware.use MAuth::Rack::RequestAuthenticator, MAUTH_CONF

Above, env is a hash of request parameters; this hash is generated by Rack. The above proc will force the middleware to authenticate only GET requests.

Another example:

MAUTH_CONF[:should_authenticate_check] = proc do |env|
  env['PATH_INFO'] == '/studies.json'
end
config.middleware.use MAuth::Rack::RequestAuthenticator, MAUTH_CONF

The above proc will force the rack middleware to authenticate only requests to the "/studies.json" path. To authenticate a group of related URIs, considered matching env['PATH_INFO'] with one or more regular expressions.

The configuration passed to the middlewares in the above examples (MAUTH_CONF) is used create a new instance of MAuth::Client. If you are managing an MAuth::Client of your own for some reason, you can pass that in on the key :mauth_client => your_client, and omit any other MAuth::Client configuration. :should_authenticate_check is handled by the middleware and should still be specified alongside :mauth_client, if you are using it.

When the request authentication middleware determines that a request is inauthentic, it will not call the application and will respond with a 401 status code along with an error, expressed in JSON (Content-Type: application/json) with the following value:

{ "errors": { "mauth": ["Unauthorized"] } }

Successfully authenticated requests will be passed to the application, as will requests for which the :should_authenticate_check condition is false.

If the middleware is unable to authenticate the request because MAuth is unavailable and so cannot serve public keys, it responds with a 500 status code and an error expressed in JSON with the value:

{ "errors": { "mauth": ["Could not determine request authenticity"] } }

Examples

Putting all this together, here are typical examples (in rails you would put that code in an initializer):

require 'mauth/rack'

MAUTH_CONF = MAuth::Client.default_config

# ResponseSigner OPTIONAL; only use if you are registered in mauth service
Rails.application.config.middleware.insert_after Rack::Runtime, MAuth::Rack::ResponseSigner, MAUTH_CONF
if Rails.env.test? || Rails.env.development?
  require 'mauth/fake/rack'
  Rails.application.config.middleware.insert_after MAuth::Rack::ResponseSigner, MAuth::Rack::RequestAuthenticationFaker, MAUTH_CONF
else
  Rails.application.config.middleware.insert_after MAuth::Rack::ResponseSigner, MAuth::Rack::RequestAuthenticatorNoAppStatus, MAUTH_CONF
end

With :should_authenticate_check:

require 'mauth/rack'

MAUTH_CONF = MAuth::Client.default_config
# authenticate all requests which pass the some_condition_of check and aren't /app_status with MAuth
MAUTH_CONF[:should_authenticate_check] = proc do |env|
  some_condition_of(env)
end

# ResponseSigner OPTIONAL; only use if you are registered in mauth service
Rails.application.config.middleware.insert_after Rack::Runtime, MAuth::Rack::ResponseSigner, MAUTH_CONF
if Rails.env.test? || Rails.env.development?
  require 'mauth/fake/rack'
  Rails.application.config.middleware.insert_after MAuth::Rack::ResponseSigner, MAuth::Rack::RequestAuthenticationFaker, MAUTH_CONF
else
  Rails.application.config.middleware.insert_after MAuth::Rack::ResponseSigner, MAuth::Rack::RequestAuthenticatorNoAppStatus, MAUTH_CONF
end

Fake middleware

For testing purposes, you may wish to use middleware which does not perform actual authentication. MAuth provides this, as MAuth::Rack::RequestAuthenticationFaker. Requests are still checked for the presence of an MAuth signature - this is necessary as many applications rely on the app_uuid identified in the signature, so it cannot be ignored entirely. However, the validity of the public key is not checked in the MAuth service, and the authenticity of the request is not verified by its signature.

This example code may augment the above examples to disable authentication in test mode:

require 'mauth/fake/rack'
authenticator = Rails.env != 'test' ? MAuth::Rack::RequestAuthenticator : MAuth::Rack::RequestAuthenticationFaker
config.middleware.use authenticator, MAUTH_CONF

Faraday Middleware Usage

If you are making outgoing HTTP requests using Faraday, adding MAuth Faraday middleware is much the same as adding rack middleware. Building your connection will look like:

Faraday.new(some_args) do |builder|
  builder.use MAuth::Faraday::RequestSigner, MAUTH_CONF
  builder.use MAuth::Faraday::ResponseAuthenticator, MAUTH_CONF
  builder.adapter Faraday.default_adapter
end

The Faraday middleware MUST come LAST in the stack of middleware. As with the rack middleware, this means it will be right next to the HTTP adapter.

Only use the MAuth::Faraday::ResponseAuthenticator middleware if you are expecting the service you are communicating with to sign its responses (all services which are aware of MAuth should be doing this).

MAUTH_CONF is the same as in Rack middleware, and as with the Rack middleware is used to initialize a MAuth::Client instance. Also as with the Rack middleware, you can pass in a MAuth::Client instance you are using yourself on the :mauth_client key, and omit any other configuration.

Both MAuth::Faraday::ResponseAuthenticator and MAuth::Faraday::RequestSigner cannot be used without a private_key and app_uuid.

If a response which does not appear to be authentic is received by the MAuth::Faraday::ResponseAuthenticator middleware, a MAuth::InauthenticError will be raised.

If the MAuth service cannot be reached, and therefore the authenticity of a response cannot be verified by ResponseAuthenticator, then a MAuth::UnableToAuthenticateError will be raised.

Other Request and Response signing

If you are not using Faraday, you will need to sign your own requests.

Instantiate a MAuth::Client with the same configuration as the middlewares, as documented on MAuth::Client#initialize. We'll call this mauth_client.

mauth_client has a method #signed_headers which takes either a MAuth::Request or MAuth::Response object, and generates HTTP headers which can be added to the request or response to indicate authenticity. Create a MAuth::Request object from the information in your HTTP request, whatever its form:

require 'mauth/request_and_response'
request = MAuth::Request.new(verb: my_verb, request_url: my_request_url, body: my_body, query_string: my_query_string)

mauth_client.signed_headers(request) will then return mauth headers which you can apply to your request.

Warning

During development classes are typically not cached in Rails applications. If this is the case, be aware that the MAuth-Client middleware object will be instantiated anew for each request; this will cause applications performing local authentication to fetch public keys before each request is authenticated.

Protocol Versions

The mauth V2 protocol was added as of v5.0.0. This protocol updates the string_to_sign to include query parameters, uses different authentication header names, and has a few other changes. See this document for more information: (DOC?). By default MAuth-Client will authenticate incoming requests with only the highest version of the protocol present, and sign their outgoing responses with only the version used to authenticate the request. By default MAuth-Client will sign outgoing requests with both the V1 and V2 protocols, and authenticate their incoming responses with only the highest version of the protocol present. If the v2_only_sign_requests flag is true all outgoing requests will be signed with only the V2 protocol (outgoing responses will still be signed with whatever protocol used to authenticate the request). If the v2_only_authenticate flag is true then MAuth-Client will reject any incoming request or incoming response that does not use the V2 protocol.