mirror of
https://github.com/team-alembic/ash_authentication.git
synced 2024-09-21 05:43:05 +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()
|
||||
def change(changeset, _opts, _context) do
|
||||
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
|
||||
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
|
||||
_ ->
|
||||
changeset
|
||||
|
|
|
@ -12,10 +12,10 @@ defmodule AshAuthentication.TokenResource.StoreConfirmationChangesChange do
|
|||
@spec change(Changeset.t(), keyword, Change.context()) :: Changeset.t()
|
||||
def change(changeset, _opts, _context) do
|
||||
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
|
||||
changeset
|
||||
|> Changeset.change_attributes(jti: jti, expires_at: expires_at)
|
||||
|> Changeset.change_attributes(jti: jti, expires_at: expires_at, subject: subject)
|
||||
else
|
||||
_ ->
|
||||
changeset
|
||||
|
|
|
@ -12,10 +12,10 @@ defmodule AshAuthentication.TokenResource.StoreTokenChange do
|
|||
@spec change(Changeset.t(), keyword, Change.context()) :: Changeset.t()
|
||||
def change(changeset, _opts, _context) do
|
||||
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
|
||||
changeset
|
||||
|> Changeset.change_attributes(jti: jti, expires_at: expires_at)
|
||||
|> Changeset.change_attributes(jti: jti, expires_at: expires_at, subject: subject)
|
||||
else
|
||||
_ ->
|
||||
changeset
|
||||
|
|
|
@ -42,6 +42,9 @@ defmodule AshAuthentication.TokenResource.Transformer do
|
|||
writable?: true
|
||||
),
|
||||
: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} <-
|
||||
maybe_build_attribute(dsl_state, :expires_at, :utc_datetime,
|
||||
allow_nil?: false,
|
||||
|
@ -153,6 +156,14 @@ defmodule AshAuthentication.TokenResource.Transformer do
|
|||
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
|
||||
with {:ok, action} <- validate_action_exists(dsl_state, action_name),
|
||||
: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)
|
||||
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{}
|
||||
|> Ecto.Changeset.cast(
|
||||
%{
|
||||
"jti" => jti,
|
||||
"subject" => subject,
|
||||
"expires_at" => DateTime.from_unix!(exp),
|
||||
"purpose" => "confirm",
|
||||
"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)
|
||||
|
||||
|
|
Loading…
Reference in a new issue