mirror of
https://github.com/team-alembic/ash_authentication.git
synced 2024-09-21 13:53:25 +12:00
improvement(TokenResource)!: Store the token subject in the token resource. (#133)
* improvement(TokenResource)!: Store the token subject in the token resource. This is a breaking change because you may have to delete tokens in your database so that you can avoid the non-null constraint on subject. * docs: Add upgrading documentation.
This commit is contained in:
parent
53d221aa6f
commit
948298ac1c
8 changed files with 165 additions and 8 deletions
30
documentation/topics/upgrading.md
Normal file
30
documentation/topics/upgrading.md
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
# Upgrading
|
||||||
|
|
||||||
|
## Upgrading to version 3.6.0.
|
||||||
|
|
||||||
|
As of version 3.6.0 the `TokenResource` extension adds the `subject` attribute
|
||||||
|
which allows us to more easily match tokens to specific users. This unlocks
|
||||||
|
some new use-cases (eg sign out everywhere).
|
||||||
|
|
||||||
|
This means that you will need to generate new migrations and migrate your
|
||||||
|
database.
|
||||||
|
|
||||||
|
### Upgrade steps:
|
||||||
|
|
||||||
|
> ### Warning {: .warning}
|
||||||
|
>
|
||||||
|
> If you already have tokens stored in your database then the migration will
|
||||||
|
> likely throw a migration error due to the new `NOT NULL` constraint on
|
||||||
|
> `subject`. If this happens then you can either delete all your tokens or
|
||||||
|
> explicitly add the `subject` attribute to your resource with `allow_nil?` set
|
||||||
|
> to `true`. eg:
|
||||||
|
>
|
||||||
|
> ```elixir
|
||||||
|
> attributes do
|
||||||
|
> attribute :subject, :string, allow_nil?: true
|
||||||
|
> end
|
||||||
|
> ```
|
||||||
|
|
||||||
|
1. Run `mix ash_postgres.generate_migrations --name=add_subject_to_token_resource`
|
||||||
|
2. Run `mix ash_postgres.migrate`
|
||||||
|
3. 🎉
|
|
@ -12,10 +12,15 @@ defmodule AshAuthentication.TokenResource.RevokeTokenChange do
|
||||||
@spec change(Changeset.t(), keyword, Change.context()) :: Changeset.t()
|
@spec change(Changeset.t(), keyword, Change.context()) :: Changeset.t()
|
||||||
def change(changeset, _opts, _context) do
|
def change(changeset, _opts, _context) do
|
||||||
with token when byte_size(token) > 0 <- Changeset.get_argument(changeset, :token),
|
with token when byte_size(token) > 0 <- Changeset.get_argument(changeset, :token),
|
||||||
{:ok, %{"jti" => jti, "exp" => exp}} <- Jwt.peek(token),
|
{:ok, %{"jti" => jti, "exp" => exp, "sub" => subject}} <- Jwt.peek(token),
|
||||||
{:ok, expires_at} <- DateTime.from_unix(exp) do
|
{:ok, expires_at} <- DateTime.from_unix(exp) do
|
||||||
changeset
|
changeset
|
||||||
|> Changeset.change_attributes(jti: jti, purpose: "revocation", expires_at: expires_at)
|
|> Changeset.change_attributes(
|
||||||
|
jti: jti,
|
||||||
|
purpose: "revocation",
|
||||||
|
expires_at: expires_at,
|
||||||
|
subject: subject
|
||||||
|
)
|
||||||
else
|
else
|
||||||
_ ->
|
_ ->
|
||||||
changeset
|
changeset
|
||||||
|
|
|
@ -12,10 +12,10 @@ defmodule AshAuthentication.TokenResource.StoreConfirmationChangesChange do
|
||||||
@spec change(Changeset.t(), keyword, Change.context()) :: Changeset.t()
|
@spec change(Changeset.t(), keyword, Change.context()) :: Changeset.t()
|
||||||
def change(changeset, _opts, _context) do
|
def change(changeset, _opts, _context) do
|
||||||
with token when byte_size(token) > 0 <- Changeset.get_argument(changeset, :token),
|
with token when byte_size(token) > 0 <- Changeset.get_argument(changeset, :token),
|
||||||
{:ok, %{"jti" => jti, "exp" => exp}} <- Jwt.peek(token),
|
{:ok, %{"jti" => jti, "exp" => exp, "sub" => subject}} <- Jwt.peek(token),
|
||||||
{:ok, expires_at} <- DateTime.from_unix(exp) do
|
{:ok, expires_at} <- DateTime.from_unix(exp) do
|
||||||
changeset
|
changeset
|
||||||
|> Changeset.change_attributes(jti: jti, expires_at: expires_at)
|
|> Changeset.change_attributes(jti: jti, expires_at: expires_at, subject: subject)
|
||||||
else
|
else
|
||||||
_ ->
|
_ ->
|
||||||
changeset
|
changeset
|
||||||
|
|
|
@ -12,10 +12,10 @@ defmodule AshAuthentication.TokenResource.StoreTokenChange do
|
||||||
@spec change(Changeset.t(), keyword, Change.context()) :: Changeset.t()
|
@spec change(Changeset.t(), keyword, Change.context()) :: Changeset.t()
|
||||||
def change(changeset, _opts, _context) do
|
def change(changeset, _opts, _context) do
|
||||||
with token when byte_size(token) > 0 <- Changeset.get_argument(changeset, :token),
|
with token when byte_size(token) > 0 <- Changeset.get_argument(changeset, :token),
|
||||||
{:ok, %{"jti" => jti, "exp" => exp}} <- Jwt.peek(token),
|
{:ok, %{"jti" => jti, "exp" => exp, "sub" => subject}} <- Jwt.peek(token),
|
||||||
{:ok, expires_at} <- DateTime.from_unix(exp) do
|
{:ok, expires_at} <- DateTime.from_unix(exp) do
|
||||||
changeset
|
changeset
|
||||||
|> Changeset.change_attributes(jti: jti, expires_at: expires_at)
|
|> Changeset.change_attributes(jti: jti, expires_at: expires_at, subject: subject)
|
||||||
else
|
else
|
||||||
_ ->
|
_ ->
|
||||||
changeset
|
changeset
|
||||||
|
|
|
@ -42,6 +42,9 @@ defmodule AshAuthentication.TokenResource.Transformer do
|
||||||
writable?: true
|
writable?: true
|
||||||
),
|
),
|
||||||
:ok <- validate_jti_field(dsl_state),
|
:ok <- validate_jti_field(dsl_state),
|
||||||
|
{:ok, dsl_state} <-
|
||||||
|
maybe_build_attribute(dsl_state, :subject, :string, allow_nil?: false, writable?: true),
|
||||||
|
:ok <- validate_subject_field(dsl_state),
|
||||||
{:ok, dsl_state} <-
|
{:ok, dsl_state} <-
|
||||||
maybe_build_attribute(dsl_state, :expires_at, :utc_datetime,
|
maybe_build_attribute(dsl_state, :expires_at, :utc_datetime,
|
||||||
allow_nil?: false,
|
allow_nil?: false,
|
||||||
|
@ -153,6 +156,14 @@ defmodule AshAuthentication.TokenResource.Transformer do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp validate_subject_field(dsl_state) do
|
||||||
|
with {:ok, resource} <- persisted_option(dsl_state, :module),
|
||||||
|
{:ok, attribute} <- find_attribute(dsl_state, :subject),
|
||||||
|
:ok <- validate_attribute_option(attribute, resource, :type, [Type.String, :string]) do
|
||||||
|
validate_attribute_option(attribute, resource, :writable?, [true])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
defp validate_get_token_action(dsl_state, action_name) do
|
defp validate_get_token_action(dsl_state, action_name) do
|
||||||
with {:ok, action} <- validate_action_exists(dsl_state, action_name),
|
with {:ok, action} <- validate_action_exists(dsl_state, action_name),
|
||||||
:ok <-
|
:ok <-
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
defmodule Example.Repo.Migrations.AddSubjectToTokenResource do
|
||||||
|
@moduledoc """
|
||||||
|
Updates resources based on their most recent snapshots.
|
||||||
|
|
||||||
|
This file was autogenerated with `mix ash_postgres.generate_migrations`
|
||||||
|
"""
|
||||||
|
|
||||||
|
use Ecto.Migration
|
||||||
|
|
||||||
|
def up do
|
||||||
|
alter table(:tokens) do
|
||||||
|
add :subject, :text, null: false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def down do
|
||||||
|
alter table(:tokens) do
|
||||||
|
remove :subject
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
89
priv/resource_snapshots/repo/tokens/20230113020413.json
Normal file
89
priv/resource_snapshots/repo/tokens/20230113020413.json
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
{
|
||||||
|
"attributes": [
|
||||||
|
{
|
||||||
|
"allow_nil?": false,
|
||||||
|
"default": "fragment(\"now()\")",
|
||||||
|
"generated?": false,
|
||||||
|
"primary_key?": false,
|
||||||
|
"references": null,
|
||||||
|
"size": null,
|
||||||
|
"source": "updated_at",
|
||||||
|
"type": "utc_datetime_usec"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_nil?": false,
|
||||||
|
"default": "fragment(\"now()\")",
|
||||||
|
"generated?": false,
|
||||||
|
"primary_key?": false,
|
||||||
|
"references": null,
|
||||||
|
"size": null,
|
||||||
|
"source": "created_at",
|
||||||
|
"type": "utc_datetime_usec"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_nil?": true,
|
||||||
|
"default": "nil",
|
||||||
|
"generated?": false,
|
||||||
|
"primary_key?": false,
|
||||||
|
"references": null,
|
||||||
|
"size": null,
|
||||||
|
"source": "extra_data",
|
||||||
|
"type": "map"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_nil?": false,
|
||||||
|
"default": "nil",
|
||||||
|
"generated?": false,
|
||||||
|
"primary_key?": false,
|
||||||
|
"references": null,
|
||||||
|
"size": null,
|
||||||
|
"source": "purpose",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_nil?": false,
|
||||||
|
"default": "nil",
|
||||||
|
"generated?": false,
|
||||||
|
"primary_key?": false,
|
||||||
|
"references": null,
|
||||||
|
"size": null,
|
||||||
|
"source": "expires_at",
|
||||||
|
"type": "utc_datetime"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_nil?": false,
|
||||||
|
"default": "nil",
|
||||||
|
"generated?": false,
|
||||||
|
"primary_key?": false,
|
||||||
|
"references": null,
|
||||||
|
"size": null,
|
||||||
|
"source": "subject",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_nil?": false,
|
||||||
|
"default": "nil",
|
||||||
|
"generated?": false,
|
||||||
|
"primary_key?": true,
|
||||||
|
"references": null,
|
||||||
|
"size": null,
|
||||||
|
"source": "jti",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"base_filter": null,
|
||||||
|
"check_constraints": [],
|
||||||
|
"custom_indexes": [],
|
||||||
|
"custom_statements": [],
|
||||||
|
"has_create_action": true,
|
||||||
|
"hash": "0936DAD6ABE123BBF0A4A703002999D621BCA7C1FAF53FE38BFCB1AAE0784449",
|
||||||
|
"identities": [],
|
||||||
|
"multitenancy": {
|
||||||
|
"attribute": null,
|
||||||
|
"global": null,
|
||||||
|
"strategy": null
|
||||||
|
},
|
||||||
|
"repo": "Elixir.Example.Repo",
|
||||||
|
"schema": null,
|
||||||
|
"table": "tokens"
|
||||||
|
}
|
|
@ -88,17 +88,18 @@ defmodule AshAuthentication.AddOn.Confirmation.ActionsTest do
|
||||||
{:ok, strategy} = Info.strategy(Example.User, :confirm)
|
{:ok, strategy} = Info.strategy(Example.User, :confirm)
|
||||||
user = build_user()
|
user = build_user()
|
||||||
|
|
||||||
{:ok, _token, %{"jti" => jti, "exp" => exp}} = Jwt.token_for_user(user)
|
{:ok, _token, %{"jti" => jti, "exp" => exp, "sub" => subject}} = Jwt.token_for_user(user)
|
||||||
|
|
||||||
%Example.Token{}
|
%Example.Token{}
|
||||||
|> Ecto.Changeset.cast(
|
|> Ecto.Changeset.cast(
|
||||||
%{
|
%{
|
||||||
"jti" => jti,
|
"jti" => jti,
|
||||||
|
"subject" => subject,
|
||||||
"expires_at" => DateTime.from_unix!(exp),
|
"expires_at" => DateTime.from_unix!(exp),
|
||||||
"purpose" => "confirm",
|
"purpose" => "confirm",
|
||||||
"extra_data" => %{"username" => username(), "hashed_password" => password()}
|
"extra_data" => %{"username" => username(), "hashed_password" => password()}
|
||||||
},
|
},
|
||||||
~w[jti expires_at purpose extra_data]a
|
~w[jti subject expires_at purpose extra_data]a
|
||||||
)
|
)
|
||||||
|> Example.Repo.insert!(on_conflict: :replace_all, conflict_target: :jti)
|
|> Example.Repo.insert!(on_conflict: :replace_all, conflict_target: :jti)
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue