defmodule Plugoid do
@moduledoc """
## 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
### 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`](https://hexdocs.pm/oidc/OIDC.ClientConfig.html) 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
- `:acr_values`: one of:
- `nil` [*Default*]: no acr values requested
- `[String.t()]`: a list of acr values
- `:acr_values_callback`: a `t: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 `t: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](#module-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 `t: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](#module-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](#module-preserving-request-parameters)
- `:prompt`: one of the standard values (`"none"`, `"login"`, `"consent"`, or
`"select_account"`)
- `:prompt_callback`: a `t: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](Plugoid.RedirectURI.html#module-determining-the-redirect-uri).
- `:response_mode`: one of:
- `"query"`
- `"fragment"`
- `"form_post"`
- `:response_mode_callback`: a `t: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 `t: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 `t: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)
## Cookie configuration
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
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](https://til.hashrocket.com/posts/cvkpwqampv-configure-max-http-header-size-in-elixir-phoenix)).
## 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
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
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](#module-security-considerations)) section.
Otherwise it falls back to the `"code"` response type.
## 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](https://openid.net/specs/openid-connect-session-1_0.html),
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
Plugoid does not support OpenID Connect logout. However, the functions:
- `Plugoid.logout/1`
- `Plugoid.logout/2`
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
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
- 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:
```elixir
config :phoenix, :filter_parameters, ["id_token", "code", "token"]
```
"""
defmodule AuthenticationRequiredError do
defexception message: "authentication is required to access this page"
end
defmodule UnauthorizedError do
defexception message: "access to this page is denied"
end
alias OIDC.Auth.OPResponseError
alias Plugoid.{
OIDCRequest,
Session.AuthSession,
Session.StateSession,
Utils
}
@behaviour Plug
@type opts :: [opt | OIDC.Auth.challenge_opt()]
@type 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()}
@type opt_callback :: (Plug.Conn.t(), opts() -> any())
@implicit_response_types ["id_token", "id_token token"]
@hybrid_response_types ["code id_token", "code token", "code id_token token"]
@impl Plug
def init(opts) do
unless opts[:issuer], do: raise("Missing issuer")
unless opts[:client_id], do: raise("Missing client_id")
unless opts[:client_config], do: raise("Missing client configuration callback")
opts
|> Keyword.put_new(:id_token_iat_max_time_gap, 30)
|> Keyword.put_new(:max_concurrent_state_session, 4)
|> Keyword.put_new(:on_unauthenticated, :auth)
|> Keyword.put_new(:on_unauthorized, :auth)
|> Keyword.put_new(:preserve_initial_request, false)
|> Keyword.put_new(:redirect_uri_callback, &__MODULE__.redirect_uri/2)
|> Keyword.put_new(:response_mode_callback, &__MODULE__.response_mode/2)
|> Keyword.put_new(:response_type_callback, &__MODULE__.response_type/2)
|> Keyword.put_new(:session_lifetime, 3600)
end
@impl Plug
def call(%Plug.Conn{private: %{plugoid_authenticated: true}} = conn, _opts) do
conn
end
def call(conn, opts) do
case Plug.Conn.fetch_query_params(conn) do
%Plug.Conn{query_params: %{"redirected" => _}} = conn ->
if opts[:preserve_initial_request] do
conn
|> Phoenix.Controller.put_view(PlugoidWeb.PreserveRequestParamsView)
|> Phoenix.Controller.render("restore.html")
|> Plug.Conn.halt()
else
conn
|> maybe_set_authenticated(opts)
|> do_call(opts)
end
%Plug.Conn{query_params: %{"oidc_error" => error_token}} ->
{:ok, token_content} =
Phoenix.Token.verify(conn, "plugoid error token", error_token, max_age: 60)
error = :erlang.binary_to_term(token_content)
respond_unauthorized(conn, error, opts)
conn ->
conn
|> maybe_set_authenticated(opts)
|> do_call(opts)
end
end
@spec do_call(Plug.Conn.t(), Plug.opts()) :: Plug.Conn.t()
defp do_call(conn, opts) do
authenticated = authenticated?(conn)
authorized = authorized?(conn, opts)
on_unauthenticated = opts[:on_unauthenticated]
on_unauthorized = opts[:on_unauthorized]
redirected = conn.query_params["redirected"] != nil
cond do
authenticated and authorized ->
conn
not authenticated and not redirected and on_unauthenticated == :auth ->
authenticate(conn, opts)
not authenticated and not redirected and on_unauthenticated == :pass ->
conn
not authenticated and not redirected and on_unauthenticated == :fail ->
respond_unauthorized(conn, %AuthenticationRequiredError{}, opts)
not authenticated and redirected and on_unauthenticated in [:auth, :fail] ->
respond_unauthorized(conn, %AuthenticationRequiredError{}, opts)
not authenticated and redirected and on_unauthenticated in :pass ->
conn
authenticated and not authorized and not redirected and on_unauthorized == :auth ->
authenticate(conn, opts)
authenticated and not authorized ->
respond_forbidden(conn, opts)
end
end
@spec maybe_set_authenticated(Plug.Conn.t(), Plug.opts()) :: Plug.Conn.t()
defp maybe_set_authenticated(conn, opts) do
case AuthSession.info(conn, opts[:issuer]) do
%AuthSession.Info{} = auth_session_info ->
now_monotonic = System.monotonic_time(:second)
if now_monotonic < auth_session_info.auth_time_monotonic + opts[:session_lifetime] do
conn
|> Plug.Conn.put_private(:plugoid_authenticated, true)
|> Plug.Conn.put_private(:plugoid_auth_iss, opts[:issuer])
|> Plug.Conn.put_private(:plugoid_auth_sub, auth_session_info.sub)
else
Plug.Conn.put_private(conn, :plugoid_authenticated, false)
end
nil ->
Plug.Conn.put_private(conn, :plugoid_authenticated, false)
end
end
@spec authorized?(Plug.Conn.t(), opts()) :: boolean()
defp authorized?(%Plug.Conn{private: %{plugoid_authenticated: true}} = conn, opts) do
%AuthSession.Info{acr: current_acr} = AuthSession.info(conn, opts[:issuer])
case opts[:claims] do
%{
"id_token" => %{
"acr" => %{
"essential" => true,
"value" => required_acr
}
}
} ->
current_acr == required_acr
%{
"id_token" => %{
"acr" => %{
"essential" => true,
"values" => acceptable_acrs
}
}
} ->
current_acr in acceptable_acrs
_ ->
true
end
end
defp authorized?(_conn, _opts) do
false
end
@spec respond_unauthorized(
Plug.Conn.t(),
OPResponseError.t() | Exception.t(),
opts()
) :: Plug.Conn.t()
defp respond_unauthorized(conn, error, opts) do
conn
|> Plug.Conn.put_status(:unauthorized)
|> Phoenix.Controller.put_view(error_view(conn, opts))
|> Phoenix.Controller.render(:"401", error: error)
|> Plug.Conn.halt()
end
@spec respond_forbidden(Plug.Conn.t(), opts()) :: Plug.Conn.t()
defp respond_forbidden(conn, opts) do
conn
|> Plug.Conn.put_status(:forbidden)
|> Phoenix.Controller.put_view(error_view(conn, opts))
|> Phoenix.Controller.render(:"403", error: %UnauthorizedError{})
|> Plug.Conn.halt()
end
@doc """
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.
"""
@spec authenticate(
Plug.Conn.t(),
opts()
) :: Plug.Conn.t()
def authenticate(conn, opts) do
opts =
Enum.reduce(
[
:acr_values,
:claims,
:id_token_hint,
:login_hint,
:prompt,
:redirect_uri,
:response_mode,
:response_type
],
opts,
&apply_opt_callback(&2, &1, conn)
)
challenge = OIDC.Auth.gen_challenge(opts)
op_request_uri = OIDC.Auth.request_uri(challenge, opts) |> URI.to_string()
conn =
StateSession.store_oidc_request(
conn,
%OIDCRequest{
challenge: challenge,
initial_request_path: conn.request_path,
initial_request_params: initial_request_params(conn, opts)
},
opts[:max_concurrent_state_session]
)
if opts[:preserve_initial_request] do
conn
|> Phoenix.Controller.put_view(PlugoidWeb.PreserveRequestParamsView)
|> Phoenix.Controller.render("save.html", conn: conn, op_request_uri: op_request_uri)
|> Plug.Conn.halt()
else
conn
|> Phoenix.Controller.redirect(external: op_request_uri)
|> Plug.Conn.halt()
end
end
@spec apply_opt_callback(opts(), atom(), Plug.Conn.t()) :: opts()
defp apply_opt_callback(opts, opt_name, conn) do
if opts[opt_name] do
opts
else
opt_callback_name = String.to_atom(Atom.to_string(opt_name) <> "_callback")
case opts[opt_callback_name] do
callback when is_function(callback, 2) ->
Keyword.put(opts, opt_name, callback.(conn, opts))
_ ->
opts
end
end
end
defp initial_request_params(conn, opts) do
if opts[:preserve_initial_request] do
%{}
else
conn.query_params
end
end
# Returns a response type supported by the OP
# In order of preference:
# - `"id_token"`: allows authentication in one unique round-trip
# - `"code"`: forces client authentication that can be considered an additional
# layer of security (when simply redirecting to an URI is not trusted)
# - or the first supported response type set in the OP metadata
@doc false
@spec response_type(Plug.Conn.t(), opts()) :: String.t()
def response_type(_conn, opts) do
response_types_supported =
Utils.server_metadata(opts)["response_types_supported"] ||
raise "Unable to retrieve `response_types_supported` from server metadata or configuration"
response_modes_supported = Utils.server_metadata(opts)["response_modes_supported"] || []
cond do
"id_token" in response_types_supported and "form_post" in response_modes_supported ->
"id_token"
"code" in response_types_supported ->
"code"
true ->
List.first(response_types_supported)
end
end
# Returns the response mode from the options
# In the implicit and hybrid flows, returns `"form_post"` if supported by the server, `"query"`
# otherwise. In the code flow, returns `nil` (the default used by the server is `"query"`).
@doc false
@spec response_mode(Plug.Conn.t(), opts()) :: String.t() | nil
def response_mode(conn, opts) do
response_type = opts[:response_type] || response_type(conn, opts)
response_modes_supported = Utils.server_metadata(opts)["response_modes_supported"] || []
if response_type in @implicit_response_types or response_type in @hybrid_response_types do
if "form_post" in response_modes_supported do
"form_post"
else
"query"
end
end
end
@doc false
@spec redirect_uri(Plug.Conn.t() | module(), opts()) :: String.t()
def redirect_uri(%Plug.Conn{} = conn, opts) do
router = Phoenix.Controller.router_module(conn)
base_redirect_uri =
apply(
Module.concat(router, Helpers),
:openid_connect_redirect_uri_url,
[Phoenix.Controller.endpoint_module(conn), :call]
)
base_redirect_uri <> "?iss=" <> URI.encode(opts[:issuer])
end
@doc """
Returns `true` if the connection is authenticated with `Plugoid`, `false` otherwise
"""
@spec authenticated?(Plug.Conn.t()) :: boolean()
def authenticated?(conn), do: conn.private[:plugoid_authenticated] == true
@doc """
Returns the issuer which has authenticated the current authenticated user, or `nil` if
the user is unauthenticated
"""
@spec issuer(Plug.Conn.t()) :: String.t() | nil
def issuer(conn), do: conn.private[:plugoid_auth_iss]
@doc """
Returns the subject (OP's "user id") of current authenticated user, or `nil` if
the user is unauthenticated
"""
@spec subject(Plug.Conn.t()) :: String.t() | nil
def subject(conn), do: conn.private[:plugoid_auth_sub]
@doc """
Returns `true` if the current request happens after a redirection from the OP, `false`
otherwise
"""
@spec redirected_from_OP?(Plug.Conn.t()) :: boolean()
def redirected_from_OP?(conn) do
case conn.params do
%{"redirected" => _} ->
true
%{"oidc_error" => _} ->
true
%{"restored" => _} ->
true
_ ->
false
end
end
@doc """
Logs out a user from an issuer
The connection should be eventually sent to have the cookie updated
"""
@spec logout(Plug.Conn.t(), OIDC.issuer()) :: Plug.Conn.t()
def logout(conn, issuer), do: AuthSession.set_unauthenticated(conn, issuer)
@doc """
Logs out a user from all issuers
The connection should be eventually sent to have the cookie unset
"""
@spec logout(Plug.Conn.t()) :: Plug.Conn.t()
def logout(conn), do: AuthSession.destroy(conn)
@spec error_view(Plug.Conn.t(), opts()) :: module()
defp error_view(conn, opts) do
case opts[:error_view] do
nil ->
Utils.error_view_from_conn(conn)
view when is_atom(view) ->
view
end
end
end