Giter Site home page Giter Site logo

infinitered / authority Goto Github PK

View Code? Open in Web Editor NEW
22.0 22.0 6.0 79 KB

Next-gen Elixir authentication specification

Home Page: https://hexdocs.pm/authority

License: MIT License

Elixir 94.57% Shell 2.56% Ruby 2.87%
authentication elixir specification

authority's People

Contributors

danielberkompas avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

authority's Issues

Authority.Authentication.after_validate/2 callback should accept the credential.

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.

Add trackable protocol

Things like

  • sign_in_count - Increased every time a sign in is made (by form, openid, oauth)
  • current_sign_in_at - A timestamp updated when the user signs in
  • last_sign_in_at - Holds the timestamp of the previous sign in
  • current_sign_in_ip - The remote ip updated when the user sign in
  • last_sign_in_ip - Holds the remote ip of the previous sign in

help me out on the this ๐Ÿ˜…

And so on could be added to Authority.Trackable protocol module.

Public API

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)

Open Questions

  • 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

Ecto and Plug.Conn utilities

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.

Authority.Ecto

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})

Authority.Plug

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 session
  • sign_out(conn) - clear the user from the session
  • plug 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 present
  • plug Authority.Plug.Unauthenticated - ensure that the loaded user is not present

I'm interested to get some opinions for how this stuff should be implemented. Should it be part of authority? A separate package? More ideas?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.