View Source Plugoid (plugoid v0.6.0)

basic-use

Basic use

defmodule MyAppWeb.Router do
  use MyAppWeb, :router
  use Plugoid.RedirectURI

  pipeline :oidc_auth do
    plug Plugoid,
      issuer: "https://repentant-brief-fishingcat.gigalixirapp.com",
      client_id: "client1",
      client_config: PlugoidDemo.OpenIDConnect.Client
  end

  scope "/private", MyAppWeb do
    pipe_through :browser
    pipe_through :oidc_auth

    get "/", PageController, :index
    post "/", PageController, :index
  end
end

plug-options

Plug options

mandatory-plug-options

Mandatory plug options

  • :client_id [Mandatory]: the client id to be used for interaction with the OpenID Provider (OP)
  • :client_config [Mandatory]: a module that implements the OIDC.ClientConfig behaviour and returns the client configuration
  • :issuer [Mandatory]: the OpenID Provider (OP) issuer. Server metadata and keys are automatically retrieved from it if the OP supports it

additional-plug-options

Additional plug options

  • :acr_values: one of:
    • nil [Default]: no acr values requested
    • [String.t()]: a list of acr values
  • :acr_values_callback: a opt_callback/0 that dynamically returns a list of ACRs. Called only if :acr_values is not set
  • :claims: the "claims" parameter
  • :claims_callback: a opt_callback/0 that dynamically returns the claim parameter. Called only if :claims is not set
  • :display: display parameter. Mostly unused. Defaults to nil
  • :error_view: the error view to be called in case of error. See the Error handling section bellow. If not set, it will be automatically set to MyApp.ErrorView where MyApp is the base module name of the application
  • :id_token_iat_max_time_gap: max time gap to accept an ID token, in seconds. Defaults to 30
  • :login_hint_callback: a opt_callback/0 that dynamically returns the login hint parameter
  • :max_age: the OIDC max age (non_neg_integer()) parameter
  • :max_concurrent_state_session: maximum of state sessions stored concurrently. Defaults to 4, set to nil for no limits. See On state cookies
  • :oauth2_metadata_updater_opts: options that will be passed to Oauth2MetadataUpdater. Some authorization server do not follow standards when forming the metadata's URI. In such a case, you might need to use the :url_construction option of Oauth2MetadataUpdater
  • :on_unauthenticated: action to be taken when the request is not authenticated. One of:
    • :auth [Default]: redirects to the authorization endpoint of the OP
    • :fail: returns an HTTP 401 error
    • :pass: hands over the request to the next plug. The request is unauthenticated (this can be checked using the authenticated?/1 function)
  • :on_unauthorized: action to be taken when the user is not authorized, because of invalid ACR. One of:
    • :auth [Default]: redirects to the authorization endpoint of the OP
    • :fail: returns an HTTP 403 error
  • :preserve_initial_request: a boolean. Defaults to false. See further Preserving request parameters
  • :prompt: one of the standard values ("none", "login", "consent", or "select_account")
  • :prompt_callback: a opt_callback/0 that dynamically returns the prompt parameter. Called only if :prompt is not set
  • :redirect_uri: the redirect URI the OP has to use for redirect. If not set, defaults to Myapp.Router.Helpers.openid_connect_redirect_uri(Myapp.Endpoint, :call) It asumes that such a route was installed. See also Plugoid.RedirectURI for automatic installation of this route and the available helpers.
  • :response_mode: one of:
    • "query"
    • "fragment"
    • "form_post"
  • :response_mode_callback: a opt_callback/0 that dynamically returns the response mode for the request. Called only if :response_mode is not set
  • :response_type: one of:
    • "code" (code flow)
    • "id_token" (implicit flow)
    • "id_token token" (implicit flow)
    • "code token" (hybrid flow)
    • "code id_token" (hybrid flow)
    • "code id_token token" (hybrid flow)
  • :response_type_callback: a opt_callback/0 that dynamically returns the response type for the request. Called only if :response_type is not set
  • :session_lifetime: the local session duration in seconds. After this time interval, the user is considered unauthenticated and is redirected again to the OP. Defaults to 3600
  • :scope: a list of scopes ([String.t()]) to be requested. The "openid" scope is automatically requested. The "offline_access" scope is to be added here if one wants OAuth2 tokens to remain active after the user's logout from the OP
  • :server_metadata: a OIDC.server_metadata/0 of server metadata that will take precedence over those of the issuer (published on the "https://issuer/.well-known/openid-configuration" URI). Useful to override one or more server metadata fields
  • ui_locales: a list of UI locales
  • :use_nonce: one of:
    • :when_mandatory [Default]: a nonce is included when using the implicit and hybrid flows
    • :always: always include a nonce (i.e. also in the code flow in which it is optional)

Plugoid uses 2 cookies, different from the Phoenix session cookie (which allows more control over the security properties of these cookies):

  • authentication cookie: stores the information about authenticated session, after being successfully redirected from the OP
  • state session: store the information about the in-flight requests to the OP. It is set before redirecting to the OP, and then used and deleted when coming back from it

It uses the standard Plug.Session.Store behaviour: any existing plug session stores can work with Plugoid.

Plugoid cookies use the following application environment options that can be configured under the :plugoid key:

  • authentication cookie:
    • :auth_cookie_name: the name of the authentication cookie. Defaults to "plugoid_auth"
    • :auth_cookie_opts: opts arg of Plug.Conn.put_resp_cookie/4. Defaults to [extra: "SameSite=Lax"]
    • :auth_cookie_store: a module implementing the Plug.Session.Store behaviour. Defaults to :ets (which is Plug.Session.ETS)
    • :auth_cookie_store_opts: options for the :auth_cookie_store. Defaults to [table: :plugoid_auth_cookie]. Note that the :plugoid_auth_cookie_store ETS table is expected to exist, i.e. to be created beforehand. It is also not suitable for production, as cookies are never deleted
  • state cookie:
    • :state_cookie_name: the base name of the state cookie. Defaults to "plugoid_state"
    • :state_cookie_opts: opts arg of Plug.Conn.put_resp_cookie/4. Defaults to [secure: true, extra: "SameSite=None"]. SameSite is set to None because OpenID Connect can redirect with a HTTP post request ("form_post" response mode) and cross-domain cookies are not sent except with this setting
    • :state_cookie_store: a module implementing the Plug.Session.Store behaviour. Defaults to :cookie (which is Plug.Session.COOKIE)
    • :state_cookie_store_opts: options for the :state_cookie_store. Defaults to []

Note that by default, :http_only is set to true as well as the :secure cookie flag if the connection is using https.

on-state-cookies

On state cookies

Plugoid allows having several in-flight requests to one or more OPs, because a user could inadvertently open 2 pages for authentication, or authenticate in parallel to several OPs (social network OIDC providers, for instance).

Also, as state cookies are by definition created by unauthenticated users, it is easy for an attacker to generate a lot of state sessions and overwhelm a relying party (the site using Plugoid), especially if the sessions are stored in the backend.

This is why it is safer to store state session on the client side. By default, Plugoid uses the :cookie session store for state sessions: in-flight OIDC requests are stored in the browser's cookies. Note that the secret key base must be set in the connection.

This, however, has the some limitations:

  • cookies are limited to 4kb of data
  • header size is also limited by web servers. Cowboy (Phoenix's web server) limits headers to 4kb as well

To deal with the first problem, Plugoid:

  • limits the amount of information stored in the state session to the minimum
  • uses different cookies for different OIDC requests ("plugoid_state_1", "plugoid_state_2", "plugoid_state_3", "plugoid_state_4" and so on)
  • limits the number of concurrent requests and deletes the older ones when needed, with the :max_concurrent_state_session option

However, the 4kb limit is still low and only a few state cookies can be stored concurrently. It is recommended to test it in your application before releasing it in production to find the right :max_concurrent_state_session. Also note that it is possible to raise this limit in Cowboy (see Configure max http header size in Elixir Phoenix).

preserving-request-parameters

Preserving request parameters

When set to true through the :preserve_initial_request option, body parameters are replayed when redirected back from the OP. This is useful to avoid losing form data when the user becomes unauthenticated while filling it.

Like for state session, it cannot be stored on server side because it would expose the server to DOS attacks (even more, as query and body parameters can be way larger). Therefore, these parameters are stored in the browser's session storage. The flow is as follows:

  • the user is not authenticated and hits a Plugoid-protected page
  • Plugoid displays a special blank page with javascript code. The javascript code stores the parameters in the session storage
  • the user is redirected to the OP (via javascript), authenticates, and is redirected to Plugoid's redirect URI
  • OIDC response is checked and, if valid, Plugoid's redirect URI plug redirects the user to the initial page
  • Plugoid displays a blank page containing javascript code, which:
    • redirects to the initial page with query parameters if the initial request was a GET request
    • builds an HTML form with initial body parameters and post it to the initial page (with query parameters as well) if the initial request was a POST request

The user is always returned to the initial page with the query parameters that existed. However, when this option is enabled, the query parameters are saved in the browser session storage instead of in a cookie, which helps saving space for long URLs.

Note that request data is stored unencrypted in the browser. If your forms may contain sensitive data, consider not using this feature. This is why this option is set to false by default.

Limitations:

  • The body must be parsed (Plug.Parsers) before reaching the Plugoid plug
  • The body's encoding must be application/x-www-form-urlencoded. File upload using the multipart/form-data as the encoding is not supported, and cannot be replayed
  • Only GET and POST request are supported ; in other cases Plugoid will fail restoring state silently

client-authentication

Client authentication

Upon registration, a client registers a unique authentication scheme to be used by itself to authenticate to the OP. In other words, a client cannot use different authentication schemes on different endpoints.

OAuth2 REST endpoints usually demand client authentication. Client authentication is handled by the TeslaOAuth2ClientAuth library. The authentication middleware to be used is determined based on the client configuration. For instance, to authenticate to the token endpoint, the "token_endpoint_auth_method" is used to determine which authentication middleware to use.

Thus, to configure a client for Basic authentication, the client configuration callback must return a configuration like:

%{
  "client_id" => "some_client_id_provided_by_the_OP",
  "token_endpoint_auth_method" => "client_secret_basic",
  "client_secret" => "<the client secret>",
  ...
}

However, the default value for the token endpoint auth method is "client_secret_basic", thus the following is enough:

%{
  "client_id" => "some_client_id_provided_by_the_OP",
  "client_secret" => "<the client secret>",
  ...
}

Also note that the implicit flow does not require client authentication.

default-responses-type-and-mode

Default responses type and mode

By default and if supported by the OP, these values are set to:

  • response mode: "form_post"
  • response type: "id_token"

These values allows direct authentication without additional roundtrip to the server, at the expense of:

  • not receiving access tokens, which is fine if only authentication is needed
  • slightly lesser security: the ID token can be replayed, while an authorization code cannot. This can be mitigated using a JTI register (see the Security considerations) section.

Otherwise it falls back to the "code" response type.

session

Session

When using OpenID Connect, the OP is authoritative to determine whether the user is authenticated or not. There are 2 ways for or Relying Party (the site using a library like Plugoid) to determine it:

  • using OpenID Connect Session Management, which is unsupported by Plugoid
  • periodically redirecting to the OP to check for authentication. If the user is authenticated on the OP, he's not asked to reauthenticate (in the browser it materializes by being swiftly redirected to the OP and back to the relying party (the site using Plugoid)).

By default, Plugoid cookies have no timeout, and are therefore session cookies. When the user closes his browser, there are destroyed.

However, another parameter is taken into account: the :session_lifetime parameter, which defaults to 1 hour. This ensures that a user can not remain indefinitely authenticated, and prevents an attacker from using a stolen cookie for too long.

That is, authenticated session cookie's lifetime is not correlated from the :session_lifetime and keeping this cookie as a session cookie is fine - it's the OP's work to handle long-lived authenticated sessions.

logout

Logout

Plugoid does not support OpenID Connect logout. However, the functions:

allow loging out a user locally by removing authenticated session data or the whole authentication cookie and session.

Note that, however, the user will be redirected again to the OP (and might be seamlessly authenticated, if his session is active on the OP) when reaching a path protected by Plugoid.

error-handling

Error handling

Errors can occur:

  • when redirected back from the OP. This is an OP error (for instance the user denied the authorization to share his personal information)
  • when analyzing the request back from the OP, if an error occurs (for instance, the ID token was expired)
  • ACR is no sufficient (user is authenticated, but not authorized)
  • when :on_unauthenticated or :on_unauthorized are set to :fail

Depending on the case, Plugoid renders one of the following templates:

  • :"401"
  • :"403"
  • :"500"

It also sets the @error assign in them to an exception, one of Plugoid or one of the OIDC library.

When the error occured on the OP, the :401 error template is called with an OIDC.Auth.OPResponseError exception.

security-considerations

Security considerations

  • Consider renaming the cookies to make it harder to detect which library is used
  • Consider setting the :domain and :path settings of the cookies
  • When using the implicit or hybrid flow, consider setting a JTI register to prevent replay attacks of ID tokens. This is configured in the Plugoid.RedirectURI plug
  • Consider filtering Phoenix's parameters in the logs. To do so, add in the configuration file config/config.exs the following line:
config :phoenix, :filter_parameters, ["id_token", "code", "token"]

Link to this section Summary

Functions

Triggers authentication by redirecting to the OP

Returns true if the connection is authenticated with Plugoid, false otherwise

Returns the issuer which has authenticated the current authenticated user, or nil if the user is unauthenticated

Logs out a user from all issuers

Logs out a user from an issuer

Returns true if the current request happens after a redirection from the OP, false otherwise

Returns the subject (OP's "user id") of current authenticated user, or nil if the user is unauthenticated

Link to this section Types

Specs

opt() ::
  {:acr_values_callback, opt_callback()}
  | {:claims_callback, opt_callback()}
  | {:error_view, module()}
  | {:id_token_hint_callback, opt_callback()}
  | {:login_hint_callback, opt_callback()}
  | {:max_concurrent_state_session, non_neg_integer() | nil}
  | {:on_unauthenticated, :auth | :fail | :pass}
  | {:on_unauthorized, :auth | :fail}
  | {:prompt_callback, opt_callback()}
  | {:redirect_uri, String.t()}
  | {:redirect_uri_callback, opt_callback()}
  | {:response_mode_callback, opt_callback()}
  | {:response_type_callback, opt_callback()}
  | {:server_metadata, OIDC.server_metadata()}
  | {:session_lifetime, non_neg_integer()}

Specs

opt_callback() :: (Plug.Conn.t(), opts() -> any())

Specs

opts() :: [opt() | OIDC.Auth.challenge_opt()]

Link to this section Functions

Link to this function

authenticate(conn, opts)

View Source

Specs

authenticate(Plug.Conn.t(), opts()) :: Plug.Conn.t()

Triggers authentication by redirecting to the OP

This function, initially only used internally, can be used to trigger redirect to the OP. This allows more fine control on when to redirect user, or to which OP redirect this user.

It is recommended to not use it if a plug-based approach can be used instead. For example, you can redirect to a Plugoid-protected route (/route/auth_with_op1) to automatically have Plugoid redirect to a specific OP, instead of using this function.

Specs

authenticated?(Plug.Conn.t()) :: boolean()

Returns true if the connection is authenticated with Plugoid, false otherwise

Specs

issuer(Plug.Conn.t()) :: String.t() | nil

Returns the issuer which has authenticated the current authenticated user, or nil if the user is unauthenticated

Specs

logout(Plug.Conn.t()) :: Plug.Conn.t()

Logs out a user from all issuers

The connection should be eventually sent to have the cookie unset

Specs

logout(Plug.Conn.t(), OIDC.issuer()) :: Plug.Conn.t()

Logs out a user from an issuer

The connection should be eventually sent to have the cookie updated

Link to this function

redirected_from_OP?(conn)

View Source

Specs

redirected_from_OP?(Plug.Conn.t()) :: boolean()

Returns true if the current request happens after a redirection from the OP, false otherwise

Specs

subject(Plug.Conn.t()) :: String.t() | nil

Returns the subject (OP's "user id") of current authenticated user, or nil if the user is unauthenticated