infinitered / authority Goto Github PK
View Code? Open in Web Editor NEWNext-gen Elixir authentication specification
Home Page: https://hexdocs.pm/authority
License: MIT License
Next-gen Elixir authentication specification
Home Page: https://hexdocs.pm/authority
License: MIT License
Given that the most common side effect I've implemented on authentication is updating/deleting the token used, I propose that we change Authority.Authentication.after_validate(user, purpose)
to Authority.Authentication.after_validate(credential, user, purpose)
defp do_authenticate(module, identifier, credential, purpose) do
with {:ok, identifier} <- module.before_identify(identifier),
{:ok, user} <- module.identify(identifier) do
credential = combine_credential(identifier, credential)
with :ok <- module.before_validate(user, purpose),
:ok <- module.validate(credential, user, purpose),
:ok <- module.after_validate(credential, user, purpose) do
{:ok, user}
else
error ->
module.failed(user, error)
error
end
end
end
This allows one to use after_validate/3
for side effects as intended. I've had to do side-effect things (such as updating a token's last_used_at
) in validate/3
, because after_validate/2
doesn't know which credential was used to validate.
Things like
help me out on the this ๐
And so on could be added to Authority.Trackable
protocol module.
Here's my proposal for what the public API of a domain using Authority would look like. Ideally, each section could be independently enabled/removed based on what the project needs.
# REGISTRATION
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
# Convert parameters into a new user account. If your registration process
# is more complicated than this, you might want a whole separate custom
# domain that calls into these functions.
# EMAIL/PASSWORD
# Register via email/password
Accounts.register(%{
email: "[email protected]",
password: "password",
password_confirmation: "password"
})
# => {:ok, %User{}}
# => {:error, %Ecto.Changeset{}}
# OAUTH CODE
Accounts.register(%OAuthCode{
provider: :facebook,
code: code
})
# => {:ok, %User{}}
# => {:error, :invalid_provider}
# => {:error, :invalid_code}
# => {:error, %Ecto.Changeset{}}
# TOKENIZE
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
# Convert credentials into a token, which represents a user and can be used
# from client apps. This will be the main public function for authentication.
# EMAIL/PASSWORD
# Convert an email/password pair into a general-purpose token. If you
# attempt this too many times, the user will be locked and you'll get a
# `Lock` struct back describing the lock.
Accounts.tokenize({email, password})
# => {:ok, %Token{purpose: :identity}}
# => {:error, :invalid_email}
# => {:error, :invalid_password}
# => {:error, %Lock{reason: :too_many_attempts, expires_at: datetime}}
# Limit a token to a specific purpose by passing it here. This is needed to
# generate single-purpose tokens like password reset tokens.
Accounts.tokenize({email, password}, purpose)
# => {:ok, %Token{purpose: purpose}}
# => {:error, :invalid_email}
# => {:error, :invalid_password}
# EMAIL/PASSWORD + 2FA
# 2FA is a two-stage process. First you submit email/password and get a 2FA
# token back. This token won't work for anything but the 2FA process.
#
# NOTE: For SMS, this should send the 2FA code via SMS to the user's phone.
{:ok, %Token{purpose: :two_factor} = two_factor_token} = Accounts.tokenize({email, password})
# When the user enters their second factor code, you submit the token again
# with their second factor code. If valid, you then get a general-purpose token
# back.
Accounts.tokenize({two_factor_token, 123_123})
# => {:ok, %Token{purpose: :identity}}
# => {:error, :invalid_token}
# => {:error, :invalid_2fa_code}
# OAUTH
# Convert an oauth provider/code pair into a user.
Accounts.tokenize(%OAuthCode{provider: :github, code: "code"})
# => {:ok, %Token{purpose: :identity}}
# => {:error, :invalid_provider}
# => {:error, :invalid_code}
# OAUTH + 2FA
# Exactly the same as email/password two-factor. First you get an intermediate
# two-factor token, which you then combine with the second factor and submit
# again.
{:ok, two_factor_token} = Accounts.tokenize(%OAuthCode{provider: :github, code: "code"})
Accounts.tokenize({two_factor_token, 123_123})
# => {:ok, %Token{purpose: :identity}}
# => {:error, :invalid_token}
# => {:error, :invalid_2fa_code}
# AUTHENTICATION
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
# Convert a credential into a user. This is really an "internal" function
# for the `MyApp` logic app. Internal domains will be calling authenticate
# to convert tokens into users.
#
# Client apps will probably call `tokenize`.
# EMAIL/PASSWORD
# Convert an email/password pair into a user. Same lock logic as above.
Accounts.authenticate({email, password})
# => {:ok, %User{}}
# => {:error, :invalid_email}
# => {:error, :invalid_password}
# => {:error, %Lock{reason: :too_many_attempts, expires_at: datetime}}
# EMAIL/PASSWORD + 2FA
# TODO: What should the `Accounts.authenticate/1` function do when you pass
# an email/password, and two-factor authentication is required for a user?
#
# Should you get a user back? Or some sort of intermediate state?
# TOKEN
# Convert a token from `tokenize` into a user.
#
# Tokens are a bit different than email/password, because they are sometimes
# only valid for some uses and not others.
#
# TODO: decide how to assert that a token is for a given purpose.
Accounts.authenticate(%Token{token: "my-token"})
# => {:ok, %User{}}
# => {:error, :invalid_token}
# OAUTH
# Convert an oauth provider/code pair into a user.
Accounts.authenticate(%OAuthCode{provider: :github, code: "code"})
# => {:ok, %User{}}
# => {:error, :invalid_provider}
# => {:error, :invalid_code}
# RECOVERY
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
# FORGOT PASSWORD
# Internally calls `tokenize(email, :recovery)` to generate a recovery token
# and email it to the user.
Accounts.recover_via_email(email)
# UNLOCK ACCOUNT
# Internally calls `tokenize(email, :unlock)` to generate an unlock token
# and email it to the user.
#
# Users would do this if they get locked out of their account for forgetting
# their password. The link would unlock their account.
Accounts.unlock_via_email(email)
Accounts.authenticate/1
: Should we allow you to specify a purpose for the authentication? Some credentials (single-purpose tokens) are not valid for all purposes. e.g.Accounts.authenticate(credential, :identity) # only general-purpose credentials allowed
Accounts.authenticate(credential, :unlock) # only :unlock credentials allowed
First, this library is fantastic. Out of the box, the Authority.Template
pretty much just works. I really appreciate the approach taken here, because it doesn't get in my way, but also doesn't make me write literally everything from scratch.
Providing a few more conveniences could help make the experience a little smoother, and would prevent the user from having to know security best practices.
I had to look at the project's test suite to see how my schemas should be setup in order to play nice with the template. This is an area where the documentation could probably be improved, but it would be nice to have some convenience changeset functions.
# Hash the password with bcrypt
put_encrypted_password(changeset, :password, :encrypted_password)
# Check that the password meets agreed-upon password strength checks
validate_secure_password(changeset, :password)
# Generate a token, and hash it with HMAC before storing it (should there be an authority function for unhashing before finding the user by token?)
put_secure_token(changeset, :token)
# Set the expires_at on the Token
put_token_expiration(changeset, :token, recovery: {24, :hours}, any: {2, :weeks})
It would be nice to have a few functions for working with Plug.Conn
. For example:
sign_in(conn, user)
- put the user in the sessionsign_out(conn)
- clear the user from the sessionplug Authority.Plug.LoadCurrentUser
- lookup the user from the session or from a token in the Authorization
header, assign conn.assigns.current_user
plug Authority.Plug.Authenticated
- ensure that the loaded user is presentplug Authority.Plug.Unauthenticated
- ensure that the loaded user is not presentI'm interested to get some opinions for how this stuff should be implemented. Should it be part of authority? A separate package? More ideas?
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.