apiac_auth_mtls v1.0.0 APIacAuthMTLS View Source

An APIac.Authenticator plug implementing section 2 of OAuth 2.0 Mutual-TLS Client Authentication and Certificate-Bound Access Tokens (RFC8705)

Using this scheme, authentication is performed thanks to 2 elements:

  • TLS client certificate authentication
  • the client_id parameter of the application/x-www-form-urlencoded body

TLS client certificate authentication may be performed thanks to two methods:

  • authentication with a certificate issued by a Certificate Authority (CA) which is called PKI Mutual-TLS Method. In this case, one of the following certificate attribute is checked against this attribute registered for the client_id:

    • Distinguished name
    • SAN DNS
    • SAN URI
    • SAN IP address
    • SAN email
  • authentication with a self-signed, self-issued certificate which is called Self-Signed Certificate Mutual-TLS Method. In this case, the certificate is checked against the subject public key info of the registered certificates of the client_id

Plug options

  • :allowed_methods: one of :pki, :selfsigned or :both. No default value, mandatory option
  • :pki_callback: a (String.t -> String.t | {tls_client_auth_subject_value(), String.t()} | nil) function that takes the client_id as a parameter and returns its DN as a String.t() or {tls_client_auth_subject_value(), String.t()} or nil if no DN is registered for that client. When no tls_client_auth_subject_value/0 is specified, defaults to :tls_client_auth_subject_dn
  • :selfsigned_callback: a (String.t -> binary() | [binary()] | nil) function that takes the client_id as a parameter and returns the certificate or the list of the certificate for the client_id, or nil if no certificate is registered for that client. Certificates can be returned in DER-encoded format, or native OTP certificate structure
  • :cert_data_origin: origin of the peer cert data. Can be set to:

    • :native: the peer certificate data is retrieved from the connection. Only works when this plug is used at the TLS termination endpoint. This is the default value
    • {:header_param, "Header-Name"}: the peer certificate data, and more specifically the parameter upon which the decision is to be made, is retrieved from an HTTP header. When using this feature, make sure that this header is filtered by a n upstream system (reverse-proxy...) so that malicious users cannot inject the value themselves. For instance, the configuration could be set to: {:header_param, "SSL_CLIENT_DN"}. If there are several values for the parameter (for instance several dNSName), they must be sent in separate headers. Not compatible with self-signed certiticate authentication
    • :header_cert: the whole certificate is forwarded in the "Client-Cert" HTTP header as a Base64 encoded value of the certificate's DER serialization, in conformance with Client-Cert HTTP Header: Conveying Client Certificate Information from TLS Terminating Reverse Proxies to Origin Server Applications (draft-bdc-something-something-certificate-01)
    • {:header_cert, "Header-Name"}: the whole certificate is forwarded in the "Header-Name" HTTP header as a Base64 encoded value of the certificate's DER serialization
    • {:header_cert_pem, "Header-Name"}: the whole certificate is forwarded in the "Header-Name" as a PEM-encoded string and retrieved by this plug
  • :set_error_response: function called when authentication failed. Defaults to APIacAuthMTLS.send_error_response/3
  • :error_response_verbosity: one of :debug, :normal or :minimal. Defaults to :normal

Example

plug APIacAuthMTLS, allowed_methods: :both,
                      selfsigned_callback: &selfsigned_certs/1,
                      pki_callback: &get_dn/1

# further

defp selfsigned_certs(client_id) do
  :ets.lookup_element(:clients, :client_id, 5)
end

defp get_dn("client-1") do
  "/C=US/ST=ARI/L=Chicago/O=Agora/CN=API access certificate"
end

defp get_dn(_), do: nil

Configuring TLS for APIacAuthMTLS authentication

Plugs can authenticate requests on elements contained in the HTTP request. Mutual TLS authentication, however, occurs on the TLS layer and the authentication context is only then passed to the plug (peer_data).

Usually, when using TLS, only the server is authenticated by the client. But client authentication by the server can also be activated on an TLS-enabled server: in this case, both the server and the clients authenticate to each other. Client authentication can either be optional or mandatory.

When a TLS-enabled server authenticates a client, it checks the client's certificate against its list of known certificate authorities (CA). CAs are trusted root certificates. The list of CAs can be changed through configuration.

Note that by default, the Erlang TLS stack does not accept self-signed certificate.

All TLS options are documented in the Erlang SSL module documentation.

Enabling TLS client authentication

This table summarizes which options are to be activated on the server:

Use-caseTLS options
No client authentication (default)(no specific option to set)
Optional client authentication-verify: :verify_peer
Mandatory client authentication-verify: :verify_peer
- fail_if_no_peer_cert: true

Example with plug_cowboy

To enable optional TLS client authentication:

Plug.Cowboy.https(MyPlug, [],
                  port: 8443,
                  keyfile: "priv/ssl/key.pem",
                  certfile: "priv/ssl/cer.pem",
                  verify: :verify_peer)

To enable mandatory TLS client authentication:

Plug.Cowboy.https(MyPlug, [],
                  port: 8443,
                  keyfile: "priv/ssl/key.pem",
                  certfile: "priv/ssl/cer.pem",
                  verify: :verify_peer,
                  fail_if_no_peer_cert: true)

Allowing TLS connection of clients with self-signed certificates

By default, Erlang's TLS stack rejects self-signed client certificates. To allow it, use the verify_fun TLS parameter with the following function:

defp verify_fun_selfsigned_cert(_, {:bad_cert, :selfsigned_peer}, user_state),
  do: {:valid, user_state}

defp verify_fun_selfsigned_cert(_, {:bad_cert, _} = reason, _),
  do: {:fail, reason}

defp verify_fun_selfsigned_cert(_, {:extension, _}, user_state),
  do: {:unkown, user_state}

defp verify_fun_selfsigned_cert(_, :valid, user_state),
  do: {:valid, user_state}

defp verify_fun_selfsigned_cert(_, :valid_peer, user_state),
  do: {:valid, user_state}

Example with plug_cowboy:

Plug.Cowboy.https(MyPlug, [],
                  port: 8443,
                  keyfile: "priv/ssl/key.pem",
                  certfile: "priv/ssl/cer.pem",
                  verify: :verify_peer,
                  verify_fun: {&verify_fun_selfsigned_cert/3, []})

Security considerations

In addition to the security considerations listed in the RFC, consider that:

  • Before TLS1.3, client authentication may leak information (further information)
  • Any CA can signe for any DN (as for any other certificate attribute). Though this is a well-known security limitation of the X509 infrastructure, issuing certificate with rogue DNs may be more difficult to detect (because less monitored)

Other considerations

When activating TLS client authentication, be aware that some browser user interfaces may prompt the user, in a unpredictable manner, for certificate selection. You may want to consider starting a TLS-authentication-enabled endpoint on another port (i.e. one port for web browsing, another one for API access).

Link to this section Summary

Functions

Saves failure in a Plug.Conn.t()'s private field and returns the conn

Link to this section Types

Link to this type

tls_client_auth_subject_value()

View Source
tls_client_auth_subject_value() ::
  :tls_client_auth_subject_dn
  | :tls_client_auth_san_dns
  | :tls_client_auth_san_uri
  | :tls_client_auth_san_ip
  | :tls_client_auth_san_email

Link to this section Functions

Link to this function

extract_credentials(conn, opts)

View Source

APIac.Authenticator credential extractor callback

The returned credentials is a {String.t, binary} tuple where:

  • the first parameter is the client_id
  • the second parameter is the raw DER-encoded certificate
Link to this function

save_authentication_failure_response(conn, error, opts)

View Source
save_authentication_failure_response(
  Plug.Conn.t(),
  %APIac.Authenticator.Unauthorized{
    __exception__: term(),
    authenticator: term(),
    reason: term()
  },
  any()
) :: Plug.Conn.t()

Saves failure in a Plug.Conn.t()'s private field and returns the conn

See the APIac.AuthFailureResponseData module for more information.

Link to this function

send_error_response(conn, error, map)

View Source

Implementation of the APIac.Authenticator callback

Verbosity

The following elements in the HTTP response are set depending on the value of the :error_response_verbosity option:

Error response verbosityHTTP StatusHeadersBody
:debugUnauthorized (401)APIac.Authenticator.Unauthorized exception's message
:normalUnauthorized (401)
:minimalUnauthorized (401)
Link to this function

validate_credentials(conn, arg, opts)

View Source

APIac.Authenticator credential validator callback

The credentials parameter must be an %X509.Certificate{} struct