Packages

An APIac authenticator plug for API authentication using the HTTP Bearer scheme

Current section

Files

Jump to
apiac_auth_bearer lib validator jwt.ex
Raw

lib/validator/jwt.ex

defmodule APIacAuthBearer.Validator.JWT do
@moduledoc """
An implementation of [RFC9068 - JSON Web Token (JWT) Profile for OAuth 2.0 Access Tokens](https://tools.ietf.org/html/draft-ietf-oauth-access-token-jwt-07).
This validator accepts the following options:
- `:issuer` **[mandatory]**: an OAuth2 issuer whose metadata and keys will be resolved
automatically
- `:client_config`: a `(APIacAuthBearer.Validator.opts() -> %{required(String.t()) => any()})`
function that returns the RS (*resource server*) configuration in case encryption is used. The
following fields are to be set:
- `"at_encrypted_response_alg"` [**mandatory**]: the algorithm used to decrypt
bearer token
- `"jwks"` [**mandatory**]: RS' symmetric or asymmetric keys used to decrypt the token
%{
"at_encrypted_response_alg" => "ECDH-ES",
"jwks" => %{
"keys" => [
%{
"crv" => "P-256",
"d" => "cNX22qgnyRI_3Ue6-2HRENiomTR6XzDK-VWtF9KJd5I",
"kty" => "EC",
"x" => "Kg0BnGxocTYC6X2kSdzEM61G-h-l70d-Xq97ZMq7RWY",
"y" => "htuiRWbDtzeZyAvezbWE31oEZiorhQiCa-792CWmPdY"
}
]
}
}
- `:oauth2_metadata_updater_opts`: options that will be passed to
`Oauth2MetadataUpdater`
- `:server_metadata`: server metadata that takes precedence over those automatically
retrieve from the server (requested from the issuer). Useful when the OP does
not support OAuth2 metadata or OpenID Connect discovery, or to override one or more
parameters
Note that the `"at_encrypted_response_alg"` parameter is **not** registered at the IANA. This
is because an OAuth2 RS is not specified as an OAuth2 client. This can be a special case of
an OAuth2 client, and is by certain AS implementations, but it's not specified as such. This
library uses the terms `:client_config` and `"at_encrypted_response_alg"` to make it easier
to use with backends that do indeed treat RSes as a special type of OAuth2 client.
The `APIacAuthBearer` `:resource_indicator` is also **mandatory** for this validator per the
specification.
"""
@behaviour APIacAuthBearer.Validator
@all_enc_enc [
"A128CBC-HS256",
"A192CBC-HS384",
"A256CBC-HS512",
"A128GCM",
"A192GCM",
"A256GCM"
]
@impl true
def validate_opts(opts) do
cond do
not is_binary(opts[:issuer]) ->
{:error, "missing or invalid mandatory option `:issuer` for #{__MODULE__}"}
not is_binary(opts[:resource_indicator]) ->
{:error, "missing or invalid mandatory option `:resource_indicator` for #{__MODULE__}"}
true ->
:ok
end
end
@impl true
def validate_bearer(bearer, opts) do
with :ok <- verify_typ(bearer),
{:ok, jws} <- maybe_decrypt(bearer, opts),
{:ok, payload_str} <- verify_signature(jws, opts),
{:ok, payload} <- Jason.decode(payload_str),
:ok <- verify_issuer(payload, opts),
# audience is verified in the main module
:ok <- verify_expiration(payload) do
{:ok, payload}
else
{:error, %Jason.DecodeError{}} ->
{:error, :invalid_jwt_inner_json_content}
{:error, _} = error ->
error
_ ->
{:error, :invalid_client_configuration}
end
end
defp verify_typ(bearer) do
cond do
JOSEUtils.is_jws?(bearer) ->
case JOSEUtils.JWS.peek_header(bearer) do
{:ok, %{"typ" => type}} when type in ["at+jwt", "application/at+jwt"] ->
:ok
_ ->
{:error, :invalid_jws_typ_header_parameter}
end
JOSEUtils.is_jwe?(bearer) ->
case JOSEUtils.JWE.peek_header(bearer) do
{:ok, %{"typ" => type}} when type in ["at+jwt", "application/at+jwt"] ->
:ok
_ ->
{:error, :invalid_jwe_typ_header_parameter}
end
true ->
{:error, :invalid_jwt_bearer}
end
end
defp maybe_decrypt(bearer, opts) do
cond do
opts[:client_config] && JOSEUtils.is_jwe?(bearer) ->
do_decrypt(bearer, opts)
JOSEUtils.is_jwe?(bearer) ->
{:error, :no_client_config_for_encrypted_token}
true ->
{:ok, bearer}
end
end
defp do_decrypt(jwe, opts) do
case opts[:client_config].(opts) do
%{"at_encrypted_response_alg" => enc_alg, "jwks" => %{"keys" => jwks}} ->
case JOSEUtils.JWE.decrypt(jwe, jwks, [enc_alg], @all_enc_enc) do
{:ok, {jws, _}} ->
{:ok, jws}
:error ->
{:error, :jwe_decryption_failure}
end
_ ->
{:error, :missing_client_config_param}
end
end
defp verify_signature(jws, opts) do
with {:ok, jwks} <- server_jwks(opts),
{:ok, %{"alg" => sig_alg}} <- JOSEUtils.JWS.peek_header(jws),
true <- sig_alg != "none",
{:ok, {payload_str, _}} <- JOSEUtils.JWS.verify(jws, jwks, [sig_alg]) do
{:ok, payload_str}
else
false ->
{:error, :illegal_use_of_sig_alg_none}
:error ->
{:error, :jws_signature_verification_failure}
{:error, %_{}} ->
{:error, :invalid_jws_header}
{:error, _} = error ->
error
end
end
defp verify_issuer(%{"iss" => iss}, opts),
do: if iss == opts[:issuer], do: :ok, else: {:error, :invalid_issuer}
defp verify_expiration(%{"exp" => exp}),
do: if exp >= System.system_time(:second), do: :ok, else: {:error, :expired_jwt_bearer}
defp server_jwks(opts) do
server_metadata = opts[:server_metadata] || %{}
issuer = opts[:issuer]
case server_metadata do
%{"jwks" => %{"keys" => jwks}} when is_list(jwks) ->
{:ok, jwks}
%{"jwks_uri" => jwks_uri} ->
JWKSURIUpdater.get_keys(jwks_uri)
_ ->
Oauth2MetadataUpdater.get_metadata(
issuer,
opts[:oauth2_metadata_updater_opts] || []
)
|> case do
{:ok, %{"jwks" => %{"keys" => jwks}}} when is_list(jwks) ->
{:ok, jwks}
{:ok, %{"jwks_uri" => jwks_uri}} ->
JWKSURIUpdater.get_keys(jwks_uri)
{:ok, _} ->
{:error, :server_has_no_jwks_configured}
{:error, _} = error ->
error
end
end
end
end