Authorizing Requests

View Source

This guide covers the security parts of OpenAPI when using Oaskit.

OpenAPI Security

OpenAPI 3.1 has support for authorization by describing security schemes and security requirements.

This section gives a quick overview of those mechanisms. Please refer to the official documentation for further details.

Security Schemes

Security schemes are defined in the components and describe how authentication and authorization should be performed by consumers of an API.

This is purely informational in Oaskit! The library does not perform any authentication using the described mechanisms; it's up to you to handle authentication with the appropriate libraries.

In the following example, the API specification describes two security schemes, api_key and oauth:

defmodule MyAppWeb.ApiSpec do
  use Oaskit

  @impl true
  def spec do
    %{
      openapi: "3.1.1",
      info: %{title: "MyApp API", version: "1.0.0"},
      components: %{
        securitySchemes: %{
          api_key: %{
            description: "an API key",
            type: "apiKey",
            name: "api-key",
            in: "header"
          },
          oauth: %{
            type: "oauth2",
            flows: %{
              authorizationCode: %{
                authorizationUrl: "https://learn.openapis.org/oauth/2.0/auth",
                tokenUrl: "https://learn.openapis.org/oauth/2.0/token",
                scopes: %{
                  "some:scope1": "Some Description",
                  "some:scope2": "Some Description"
                }
              }
            }
          }
        }
      }
      # ... paths, servers, etc.
    }
  end
end

Operation Level Security

OpenAPI operations accept a security option. These are security requirements associating the name of a security scheme.

This option on operations is read by Oaskit, but has to be handled by a custom plug. Plug integration is described later in this document.

The :security option on the operation macro accepts a list of maps where the map keys are names of the security schemes, and values are lists of required roles or scopes. OpenAPI uses "roles" and "scopes" interchangeably depending on the type of authentication.

In the following example, the operation should only be allowed for users authenticated with the api_key security scheme with the post:read and post:create roles.

operation :create_post,
  operation_id: "CreatePost",
  request_body: PostSchema,
  security: [%{api_key: ["post:read", "post:create"]}]

def create_post(conn, _) do
  # ...
end

Root Level Security

Security can be defined at the root level of the specification. In that case, it applies to all operations that do not define the security option themselves. The following example requires requests to all operations to be authenticated with the api_key security scheme, but does not enforce any particular scope.

defmodule MyAppWeb.ApiSpec do
  use Oaskit

  @impl true
  def spec do
    %{
      openapi: "3.1.1",
      info: %{title: "MyApp API", version: "1.0.0"},
      components: %{
        securitySchemes: %{
          api_key: %{...}
        }
      },
      security: [%{api_key: []}]
    }
  end
end

When global security requirements are defined, operations can opt out of those requirements by defining an empty list in the security option:

To make security optional, an empty security requirement ({}) can be included in the array. This definition overrides any declared top-level security. To remove a top-level security declaration, an empty array can be used.

source

operation :create_post,
  operation_id: "CreatePost",
  request_body: PostSchema,
  security: []

def create_post(conn, _) do
  # ...
end

Operations can also define more restrictive or different security requirements. Remember that once the option is defined on an operation, the root-level option is discarded for that operation. If the root-level requirements must be respected, they must be repeated in the operation-level option requirements.

Plug Integration

When the security option is defined on an operation (or globally at the root level), Oaskit expects a custom plug to handle security verifications. Oaskit does not know how to verify anything by itself.

Your plug must be set in the :security option when plugging Oaskit.Plugs.ValidateRequest:

plug Oaskit.Plugs.ValidateRequest, security: MyApp.Plugs.ApiSecurity

Plug Options

Using a module

When defined as a module, your plug will receive all options passed to the request validation plug:

plug Oaskit.Plugs.ValidateRequest,
  security: MyApp.Plugs.ApiSecurity,
  pretty_errors: true,
  html_errors: false,
  custom_opt: "foo"

This plug will receive all other options, including the custom option that Oaskit does not know about.

Using a module and custom options

It is also possible to pass options to the plug explicitly. In that case, the plug will not receive any other options given to the request validation plug.

plug Oaskit.Plugs.ValidateRequest,
  # ... other options
  security: {MyApp.Plugs.ApiSecurity, log_level: :debug}

Extra options

Besides the plug level options, when invoked, your plug will receive two extra options:

  • The :operation_id from the operation to authorize.
  • The :security requirements from the operation to authorize, defined on the operation or inherited from the root-level security.

Because of this, the options for plugs passed as a {module, options} tuple must always be a keyword list, so the operation ID and the security requirements can be injected.

Disabling security

The operations :security option must be handled

If no custom plug is defined but one of your operations defines the :security option, or the root-level :security option is defined, Oaskit will default to returning a 401 response with a raw "unauthorized" body.

This is made so nobody will expect that the security will be automatically enforced although Oaskit cannot know how to do it.

Security requirements vary greatly between applications, and we want to make it explicit that you are responsible for protecting your API endpoints.

If you would rather implement authorization by other means, you can opt out of the security mechanism by passing false instead of a plug to the request validation plug:

plug Oaskit.Plugs.ValidateRequest,
  security: false

Security Lifecycle

Authentication

Authentication should not be handled in Oaskit validation. You will generally use libraries such as "mix phx.gen.auth", Pow, or Guardian to authenticate requests.

These libraries use plugs at the endpoint or router level, well before the request's conn enters any Oaskit code.

In general, those plugs will store user information in conn.private or conn.assigns. This will be helpful to perform authorization with Oaskit. Make sure that the roles or scopes are available in the conn data, or can be retrieved easily from that data.

Authorization

Your custom plug used in Oaskit security will be called with the operation ID and the security requirements. This is a good place to validate authorization by comparing the requirements with the user information stored in the conn.

  • The init/1 callback of your plug is first called with the options described in the related section above.
  • The call/2 callback of your plug is then called with the conn and the result of the init/1 function.

Note that security validation happens before all other validations performed by the Oaskit.Plugs.ValidateRequest plug. This prevents potential attack vectors through the validation process itself. As a consequence, the Oaskit.Controller.body_params/1 and other helpers cannot be used from your plug as those cast values are not yet defined in the conn.

Authorization Logic Implementation

Your plug should implement the expected behavior from OpenAPI, but it's up to you!

A Security Requirement Object MAY refer to multiple security schemes in which case all schemes MUST be satisfied for a request to be authorized. This enables support for scenarios where multiple query parameters or HTTP headers are required to convey security information.

When the security field is defined on the OpenAPI Object or Operation Object and contains multiple Security Requirement Objects, only one of the entries in the list needs to be satisfied to authorize the request. This enables support for scenarios where the API allows multiple, independent security schemes.

source

Requirements Examples

In the following example, the request should be authenticated with both api_key and oauth (which may not make sense but it's an example!):

operation :create_post,
  security: [
    %{
      api_key: ["post:read", "post:create"],
      oauth: ["posts"]
    }
  ]

In the following example, only one of the two authentication mechanisms is required to allow the request.

operation :create_post,
  security: [
    %{api_key: ["post:read", "post:create"]},
    %{oauth: ["posts"]}
  ]

After careful inspection of the incoming request, your plug can now return the conn, either halted or not.

Your security plug must halt the conn to prevent unauthorized access. The Oaskit.Plugs.ValidateRequest plug checks the :halted property of the conn:

  • If halted: The request is stopped immediately. No further validation occurs and the controller action is not called.
  • If not halted: The request validation continues (body, params, etc.) and your controller will be called if validation succeeds.

To halt the conn, use the Plug.Conn.halt/1 function:

conn
|> put_status(401)
|> json(%{error: "Unauthorized"})
|> halt()

When the conn is halted, Phoenix or Plug will stop any further processing. This means you can do anything with it, like rendering a custom JSON template or redirecting to an authentication provider (provided the API consumer knows how to handle that).

After Authorization

If your custom plug returns a non-halted conn, the validation process follows with body and parameters validation, as usual, before reaching your controller action.

Operations Without Security

The security plug is only called for operations that explicitly define the :security option or when the root-level security is defined.

  • Operations without this option will skip security validation entirely, unless security requirements are defined at the root level of your OpenAPI specification.
  • Operations that define an empty security requirement list to override root-level security will trigger the security mechanism and your plug will receive an empty list in the :security option.