-
Notifications
You must be signed in to change notification settings - Fork 6
Authentication Draft #2 #4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
I reworked `Authenticate` to rely on internal callbacks rather than an
external "store" module. This feels more natural to me, because it's
more like a GenServer. The distinction between the authentication module
and the store felt arbitrary.
My goal is to get the callback system right, because that's what you'll
need to use whenever you want to do something custom.
After that, we can examine if we need to add an additional layer on top
for common configurations.
Here's an example of how you might implement an `Accounts` domain which
supports email/password and tokenization.
```elixir
defmodule MyApp.Accounts do
alias MyApp.Accounts.{
Authentication,
Tokenization
}
defdelegate authenticate(credential, purpose \\ :any), to: Authentication
defdelegate tokenize(credential, purpose \\ :any), to: Tokenization
end
defmodule MyApp.Accounts.Authentication do
use Authority.Authentication
alias MyApp.{
Accounts.User,
Accounts.Token,
Repo
}
# Refresh the token from the `token` attribute, so that you
# don't have to pass the full token
def before_lookup(%Token{token: token}) do
token =
Token
|> Repo.get_by(token: token)
|> Repo.preload(:user)
case token do
nil -> {:error, :invalid_token}
token -> {:ok, token}
end
end
def before_lookup(identifier), do: {:ok, identifier}
def lookup(%Token{user: %User{} = user}) do
{:ok, user}
end
def lookup(email) do
case Repo.get_by(User, email: email) do
nil -> {:error, :invalid_email}
user -> {:ok, user}
end
end
def validate(%Token{expires_at: expires_at, purpose: _purpose}, _user, _purpose) do
if DateTime.compare(DateTime.utc_now(), expires_at) == :lt do
:ok
else
{:error, :expired_token}
end
end
def validate(%Token{}, _user, _purpose) do
{:error, :invalid_token_for_purpose}
end
def validate(password, user, _purpose) do
case Comeonin.Bcrypt.checkpw(password, user.encrypted_password) do
true -> :ok
false -> {:error, :invalid_password}
end
end
end
defmodule MyApp.Accounts.Tokenization do
use Authority.Tokenization
alias MyApp.Accounts.Authentication
def tokenize({email, password}, purpose) do
with {:ok, user} <- Authentication.authenticate({email, password}, purpose) do
do_tokenize(user, purpose)
end
end
def tokenize(email, :recovery) do
do_tokenize(user, purpose)
end
def tokenize(_other, _purpose) do
{:error, :invalid_credential_for_purpose}
end
defp do_tokenize(user, purpose) do
%Token{user: user}
|> Token.insert_changeset(%{purpose: purpose})
|> Repo.insert()
end
end
```
As you can see, it isn't a lot of code. Perhaps we could do a few projects this
way, and then see if we feel like we really need an extra layer on top
for common configurations.
1694b06 to
2882d9b
Compare
|
I added some templates that show how I think defmodule MyApp.Accounts do
use Authority.Template.Authenticate,
repo: MyApp.Repo,
user_module: MyApp.Accounts.User
endIf you need tokenization: defmodule MyApp.Accounts do
use Authority.Template.AuthenticateTokenize,
repo: MyApp.Repo,
user_module: MyApp.Accounts.User,
token_module: MyApp.Accounts.Token
endThere are more options you can pass in each example, but they have sensible defaults. Ideally, I'd like for you not to specify exactly which template you are using, but rather which features (modules) you want. Something like: defmodule MyApp.Accounts do
use Authority.Template,
modules: [Authority.Authentication, Authority.Tokenization],
config: %{
repo: MyApp.Repo,
# ...
}
endThis would decide what template you need and inject it, passing on the configuration. |
3891fd0 to
543f4ba
Compare
This demonstrates how we can easily make templates out of common use cases which you can just drop into your projects.
543f4ba to
16773bf
Compare
|
I added Email/Password Authenticationdefmodule MyApp.Accounts do
use Authority.Template,
behaviours: [Authority.Authentication],
config: [repo: MyApp.Repo, user_module: MyApp.Accounts.User]
endEmail/Password + Tokenizationdefmodule MyApp.Accounts do
use Authority.Template,
behaviours: [Authority.Authentication, Authority.Tokenization],
config: [
repo: MyApp.Repo,
user_module: MyApp.Accounts.User,
token_module: MyApp.Accounts.Token
]
end |
|
@danielberkompas Thanks for adding the templates - that makes it easier to see everything in the context of an end user. I think this looks good, and agree that it's ready to start using on actual projects. That will be the only way to know if we've got the design right. |
I reworked
Authenticateto rely on internal callbacks rather than anexternal "store" module. This feels more natural to me, because it's
more like a GenServer. The distinction between the authentication module
and the store felt arbitrary.
My goal is to get the callback system right, because that's what you'll
need to use whenever you want to do something custom.
After that, we can examine if we need to add an additional layer on top
for common configurations.
Here's an example of how you might implement an
Accountsdomain whichsupports email/password and tokenization.
As you can see, it isn't a lot of code. Perhaps we could do a few projects this
way, and then see if we feel like we really need an extra layer on top
for common configurations.