mirror of
https://github.com/team-alembic/ash_authentication.git
synced 2024-09-19 12:52:55 +12:00
improvement(Jwt)!: Use token signing secret into the DSL.
Use the `AshAuthentication.Secret` behaviour, rather than asking the user to explicitly set it in their application environment. This is a breaking change that will require folks to change their resource config to look up the signing secret. Closes #79. Closes #77.
This commit is contained in:
parent
6977c39990
commit
371a6ad821
9 changed files with 121 additions and 58 deletions
|
@ -26,9 +26,6 @@ config :ash_authentication, Example,
|
|||
registry: Example.Registry
|
||||
]
|
||||
|
||||
config :ash_authentication, AshAuthentication.Jwt,
|
||||
signing_secret: "Marty McFly in the past with the Delorean"
|
||||
|
||||
config :ash_authentication,
|
||||
authentication: [
|
||||
strategies: [
|
||||
|
@ -41,5 +38,8 @@ config :ash_authentication,
|
|||
token_path: "/oauth/token",
|
||||
user_path: "/userinfo"
|
||||
]
|
||||
],
|
||||
tokens: [
|
||||
signing_secret: "Marty McFly in the past with the Delorean"
|
||||
]
|
||||
]
|
||||
|
|
|
@ -19,9 +19,6 @@ config :bcrypt_elixir, :log_rounds, 4
|
|||
|
||||
config :ash, :disable_async?, true
|
||||
|
||||
config :ash_authentication, AshAuthentication.Jwt,
|
||||
signing_secret: "Marty McFly in the past with the Delorean"
|
||||
|
||||
config :ash_authentication,
|
||||
authentication: [
|
||||
strategies: [
|
||||
|
@ -34,5 +31,8 @@ config :ash_authentication,
|
|||
token_path: "/oauth/token",
|
||||
user_path: "/userinfo"
|
||||
]
|
||||
],
|
||||
tokens: [
|
||||
signing_secret: "Marty McFly in the past with the Delorean"
|
||||
]
|
||||
]
|
||||
|
|
|
@ -116,7 +116,10 @@ end
|
|||
Next, let's define our `Token` resource. This resource is needed
|
||||
if token generation is enabled for any resources in your application. Most of
|
||||
the contents are auto-generated, so we just need to provide the data layer
|
||||
configuration and the API to use:
|
||||
configuration and the API to use.
|
||||
|
||||
You can skip this step if you don't want to use tokens, in which case remove the
|
||||
`tokens` DSL section in the user resource below.
|
||||
|
||||
```elixir
|
||||
# lib/my_app/accounts/token.ex
|
||||
|
@ -137,7 +140,8 @@ defmodule MyApp.Accounts.Token do
|
|||
end
|
||||
```
|
||||
|
||||
Lastly let's define our `User` resource, using password authentication and token generation enabled.
|
||||
Lastly let's define our `User` resource, using password authentication and token
|
||||
generation enabled.
|
||||
|
||||
```elixir
|
||||
# lib/my_app/accounts/user.ex
|
||||
|
@ -165,6 +169,9 @@ defmodule MyApp.Accounts.User do
|
|||
tokens do
|
||||
enabled? true
|
||||
token_resource MyApp.Accounts.Token
|
||||
signing_secret fn _, _ ->
|
||||
Application.fetch_env(:my_app, :token_signing_secret)
|
||||
end
|
||||
do
|
||||
end
|
||||
|
||||
|
@ -187,7 +194,8 @@ Now we have enough in place to register and sign-in users using the
|
|||
|
||||
## Plugs and routing
|
||||
|
||||
If you're using Phoenix, then you can skip this section and go straight to {{link:ash_authentication:guide:getting_started_02_phoenix|Using with Phoenix}}
|
||||
If you're using Phoenix, then you can skip this section and go straight to
|
||||
{{link:ash_authentication:guide:getting_started_02_phoenix|Using with Phoenix}}
|
||||
|
||||
In order for your users to be able to sign in, you will likely need to provide
|
||||
an HTTP endpoint to submit credentials or OAuth requests to. Ash Authentication
|
||||
|
@ -268,6 +276,24 @@ defmodule MyApp.Application do
|
|||
end
|
||||
```
|
||||
|
||||
## Token generation
|
||||
|
||||
If you have token generation enabled then you need to provide (at minimum) a
|
||||
signing secret. As the name implies this should be a secret. AshAuthentication
|
||||
provides a mechanism for looking up secrets at runtime using the
|
||||
`AshAuthentication.Secret` behaviour. To save you a click, this means that you
|
||||
can set your token signing secret using either a static string (please don't!),
|
||||
a two-arity anonymous function, or a module which implements the
|
||||
`AshAuthentication.Secret` behaviour.
|
||||
|
||||
At it's simplest you should so something like this:
|
||||
|
||||
```
|
||||
signing_secret fn _, _ ->
|
||||
Application.fetch_env(:my_app, :token_signing_secret)
|
||||
end
|
||||
```
|
||||
|
||||
## Summary
|
||||
|
||||
In this guide we've learned how to install Ash Authentication, configure
|
||||
|
|
|
@ -149,6 +149,14 @@ defmodule AshAuthentication.Dsl do
|
|||
confirmations.
|
||||
""",
|
||||
required: true
|
||||
],
|
||||
signing_secret: [
|
||||
type: @secret_type,
|
||||
doc: """
|
||||
The secret used to sign tokens.
|
||||
|
||||
#{@secret_doc}
|
||||
"""
|
||||
]
|
||||
]
|
||||
},
|
||||
|
|
|
@ -12,30 +12,47 @@ defmodule AshAuthentication.Jwt do
|
|||
There are a few things we need to know in order to generate and sign a JWT:
|
||||
|
||||
* `signing_algorithm` - the crypographic algorithm used to to sign tokens.
|
||||
Instance-wide configuration is configured by the application environment,
|
||||
but can be overriden on a per-resource basis.
|
||||
* `token_lifetime` - how long the token is valid for (in hours).
|
||||
Instance-wide configuration is configured by the application environment,
|
||||
but can be overriden on a per-resource basis.
|
||||
* `signing_secret` - the secret key used to sign the tokens. Only
|
||||
configurable via the application environment.
|
||||
* `signing_secret` - the secret key used to sign the tokens.
|
||||
|
||||
These can be configured in your resource's token DSL:
|
||||
|
||||
```elixir
|
||||
config :ash_authentication, #{inspect(__MODULE__)},
|
||||
signing_algorithm: #{inspect(@default_algorithm)}
|
||||
signing_secret: "I finally invent something that works!",
|
||||
token_lifetime: #{@default_lifetime_days * 24} # #{@default_lifetime_days} days
|
||||
defmodule MyApp.Accounts.User do
|
||||
# ...
|
||||
|
||||
authentication do
|
||||
tokens do
|
||||
enabled? true
|
||||
token_lifetime 32
|
||||
signing_secret fn _, _ ->
|
||||
System.fetch_env("TOKEN_SIGNING_SECRET")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# ...
|
||||
end
|
||||
```
|
||||
|
||||
The signing secret is retrieved using the `AshAuthentication.Secret`
|
||||
behaviour, which means that it can be retrieved one of three ways:
|
||||
|
||||
1. As a string directly in your resource DSL (please don't do this unless you
|
||||
know why this is a bad idea!), or
|
||||
2. a two-arity anonymous function which returns `{:ok, secret}`, or
|
||||
3. the name of a module which implements the `AshAuthentication.Secret`
|
||||
behaviour.
|
||||
|
||||
Available signing algorithms are #{to_sentence(@supported_algorithms, final: "or")}. Defaults to #{@default_algorithm}.
|
||||
|
||||
We strongly advise against storing the signing secret in your mix config. We
|
||||
instead suggest you make use of
|
||||
We strongly advise against storing the signing secret in your mix config or
|
||||
directly in your resource configuration. We instead suggest you make use of
|
||||
[`runtime.exs`](https://elixir-lang.org/getting-started/mix-otp/config-and-releases.html#configuration)
|
||||
and read it from the system environment or other secret store.
|
||||
|
||||
The default token lifetime is #{@default_lifetime_days * 24} and should be specified
|
||||
in integer positive hours.
|
||||
The default token lifetime is #{@default_lifetime_days * 24} and should be
|
||||
specified in integer positive hours.
|
||||
"""
|
||||
|
||||
alias Ash.Resource
|
||||
|
|
|
@ -16,10 +16,13 @@ defmodule AshAuthentication.Jwt.Config do
|
|||
"""
|
||||
@spec default_claims(Resource.t(), keyword) :: Joken.token_config()
|
||||
def default_claims(resource, opts \\ []) do
|
||||
config =
|
||||
resource
|
||||
|> config()
|
||||
|> Keyword.merge(opts)
|
||||
token_lifetime =
|
||||
opts
|
||||
|> Keyword.fetch(:token_lifetime)
|
||||
|> case do
|
||||
{:ok, hours} -> hours * 60 * 60
|
||||
:error -> token_lifetime(resource)
|
||||
end
|
||||
|
||||
{:ok, vsn} = :application.get_key(:ash_authentication, :vsn)
|
||||
|
||||
|
@ -28,7 +31,7 @@ defmodule AshAuthentication.Jwt.Config do
|
|||
|> to_string()
|
||||
|> Version.parse!()
|
||||
|
||||
Config.default_claims(default_exp: token_lifetime(config))
|
||||
Config.default_claims(default_exp: token_lifetime)
|
||||
|> Config.add_claim(
|
||||
"iss",
|
||||
fn -> generate_issuer(vsn) end,
|
||||
|
@ -111,38 +114,42 @@ defmodule AshAuthentication.Jwt.Config do
|
|||
"""
|
||||
@spec token_signer(Resource.t(), keyword) :: Signer.t()
|
||||
def token_signer(resource, opts \\ []) do
|
||||
config =
|
||||
resource
|
||||
|> config()
|
||||
|> Keyword.merge(opts)
|
||||
algorithm =
|
||||
with :error <- Keyword.fetch(opts, :signing_algorithm),
|
||||
:error <- Info.authentication_tokens_signing_algorithm(resource) do
|
||||
Jwt.default_algorithm()
|
||||
else
|
||||
{:ok, algorithm} -> algorithm
|
||||
end
|
||||
|
||||
algorithm = Keyword.get_lazy(config, :signing_algorithm, &Jwt.default_algorithm/0)
|
||||
signing_secret =
|
||||
with :error <- Keyword.fetch(opts, :signing_secret),
|
||||
{:ok, {secret_module, secret_opts}} <-
|
||||
Info.authentication_tokens_signing_secret(resource),
|
||||
{:ok, secret} <-
|
||||
secret_module.secret_for(
|
||||
~w[authentication tokens signing_secret]a,
|
||||
resource,
|
||||
secret_opts
|
||||
) do
|
||||
secret
|
||||
else
|
||||
{:ok, secret} when is_binary(secret) ->
|
||||
secret
|
||||
|
||||
case Keyword.fetch(config, :signing_secret) do
|
||||
{:ok, secret} ->
|
||||
Signer.create(algorithm, secret)
|
||||
_ ->
|
||||
raise "Missing JWT signing secret. Please see the documentation for `AshAuthentication.Jwt` for details"
|
||||
end
|
||||
|
||||
:error ->
|
||||
raise "Missing JWT signing secret. Please see the documentation for `AshAuthentication.Jwt` for details"
|
||||
Signer.create(algorithm, signing_secret)
|
||||
end
|
||||
|
||||
defp token_lifetime(resource) do
|
||||
resource
|
||||
|> Info.authentication_tokens_token_lifetime()
|
||||
|> case do
|
||||
{:ok, hours} -> hours * 60 * 60
|
||||
:error -> Jwt.default_lifetime_hrs() * 60 * 60
|
||||
end
|
||||
end
|
||||
|
||||
defp token_lifetime(config) do
|
||||
hours =
|
||||
config
|
||||
|> Keyword.get_lazy(:token_lifetime, &Jwt.default_lifetime_hrs/0)
|
||||
|
||||
hours * 60 * 60
|
||||
end
|
||||
|
||||
defp config(resource) do
|
||||
config =
|
||||
resource
|
||||
|> Info.authentication_tokens_options()
|
||||
|> Enum.reject(&is_nil(elem(&1, 1)))
|
||||
|
||||
:ash_authentication
|
||||
|> Application.get_env(Jwt, [])
|
||||
|> Keyword.merge(config)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -41,7 +41,7 @@ defmodule AshAuthentication.Secret do
|
|||
|
||||
strategies do
|
||||
oauth2 do
|
||||
client_id fn _secret, _resource, _opts ->
|
||||
client_id fn _secret, _resource ->
|
||||
Application.fetch_env(:my_app, :oauth_client_id)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -56,6 +56,10 @@ defmodule AshAuthentication.SparkDocIndex do
|
|||
AshAuthentication.Strategy,
|
||||
AshAuthentication.Strategy.Password,
|
||||
AshAuthentication.Strategy.OAuth2
|
||||
]},
|
||||
{"Add Ons",
|
||||
[
|
||||
AshAuthentication.AddOn.Confirmation
|
||||
]}
|
||||
]
|
||||
end
|
||||
|
|
|
@ -103,6 +103,7 @@ defmodule Example.User do
|
|||
tokens do
|
||||
enabled?(true)
|
||||
token_resource(Example.Token)
|
||||
signing_secret(&get_config/2)
|
||||
end
|
||||
|
||||
add_ons do
|
||||
|
|
Loading…
Reference in a new issue