Giter Site home page Giter Site logo

multi_auth's Introduction

MultiAuth

Build Status

MultiAuth is a library that standardizes multi-provider authentication for web applications. Currently supported providers:

Installation

Add this to your application's shard.yml:

dependencies:
  multi_auth:
    github: msa7/multi_auth

Usage

MultiAuth public interface

  require "multi_auth"

  MultiAuth.config("github", ENV['ID'], ENV['SECRET']) # configuration

  multi_auth = MultiAuth.make(provider, redirect_uri) # initialize engine
  multi_auth.authorize_uri  # URL to provider authentication dialog

  # on http callback, like /multi_auth/github/callback
  user = multi_auth.user(params) # get signed in user

MultiAuth build with no dependency, it can be used with any web framework. Information about signed in user described in User class here src/multi_auth/user.cr. Supported providers src/multi_auth/providers. I hope it easy to add new providers.

Kemal integration example

<a href="/multi_auth/github">Sign in with Github</a>
MultiAuth.config("facebook", "facebookClientID", "facebookSecretKey")
MultiAuth.config("google", "googleClientID", "googleSecretKey")

def self.multi_auth(env)
  provider = env.params.url["provider"]
  redirect_uri = "#{Kemal.config.scheme}://#{env.request.host_with_port.as(String)}/multi_auth/#{provider}/callback"
  MultiAuth.make(provider, redirect_uri)
end

get "/multi_auth/:provider" do |env|
  env.redirect(multi_auth(env).authorize_uri)
end

get "/multi_auth/:provider/callback" do |env|
  user = multi_auth(env).user(env.params.query)
  p user.email
  user
end

Lucky integration example

# config/watch.yml
host: myapp.lvh.me
port: 5000

# config/multi_auth_handler.cr
require "multi_auth"

class MultiAuthHandler
  MultiAuth.config("facebook", "facebookClientID", "facebookSecretKey")
  MultiAuth.config("google", "googleClientID", "googleSecretKey")

  def self.authorize_uri(provider : String)
    MultiAuth.make(provider, "#{Lucky::RouteHelper.settings.base_uri}/oauth/#{provider}/callback").authorize_uri(scope: "email")
  end

  def self.user(provider : String, params : Enumerable({String, String}))
    MultiAuth.make(provider, "#{Lucky::RouteHelper.settings.base_uri}/oauth/#{provider}/callback").user(params)
  end
end

# src/actions/oauth/handler.cr
class OAuth::Handler < BrowserAction
  get "/oauth/:provider" do
    redirect to: MultiAuthHandler.authorize_uri(provider)
  end
end

# src/actions/oauth/handler/callback.cr
class OAuth::Handler::Callback < BrowserAction
  get "/oauth/:provider/callback" do
    user = MultiAuthHandler.user(provider, request.query_params)
    text user.email.to_s
  end
end

Amber integration example

# config/initializers/multi_auth.cr
require "multi_auth"

MultiAuth.config("facebook", "facebookClientID", "facebookSecretKey")
MultiAuth.config("google", "googleClientID", "googleSecretKey")

# config/routes.cr
routes :web do
  ...
  get "/multi_auth/:provider", MultiAuthController, :new
  get "/multi_auth/:provider/callback", MultiAuthController, :callback
end

# src/controllers/multi_auth_controller.cr
class MultiAuthController < ApplicationController
  def new
    redirect_to multi_auth.authorize_uri(scope: "email")
  end

  def callback
    multi_auth_user = multi_auth.user(request.query_params)

    if user = User.find_by email: multi_auth_user.email
      login user
    else
      user = User.create!(
        first_name: multi_auth_user.first_name,
        last_name: multi_auth_user.last_name,
        email: multi_auth_user.email
      )
      login user
    end

    redirect_to "/"
  end

  def login(user)
    context.session["user_id"] = user.id
  end

  def provider
    params[:provider]
  end

  def redirect_uri
    "#{Amber.settings.secrets["base_url"]}/multi_auth/#{provider}/callback"
  end

  def multi_auth
    MultiAuth.make(provider, redirect_uri)
  end
end

Marten integration example

# config/initializers/multi_auth.cr
# ----

require "multi_auth"

MultiAuth.config("github", "<github_client_id>", "<github_secret_key>")


# config/routes.cr
# ----

Marten.routes.draw do
  path "/oauth/<provider:string>", OAuthInitiateHandler, name: "oauth_initiate"
  path "/oauth/<provider:string>/callback", OAuthCallbackHandler, name: "oauth_callback"
end


# src/handlers/concerns/with_oauth.cr
# ----

module WithOAuth
  def multi_auth
    MultiAuth.make(provider, redirect_uri)
  end

  private def provider
    params["provider"].to_s
  end

  private def redirect_uri
    "#{request.scheme}://#{request.host}#{reverse("oauth_callback", provider: provider)}"
  end
end


# src/handlers/oauth_initiate_handler.cr
# ----

require "./concerns/**"

class OAuthInitiateHandler < Marten::Handler
  include WithOAuth

  def get
    redirect multi_auth.authorize_uri(scope: "email")
  end
end


# src/handlers/oauth_initiate_callback.cr
# ----

require "./concerns/**"

class OAuthCallbackHandler < Marten::Handler
  include WithOAuth

  def get
    user_params = Hash(String, String).new.tap do |params|
      request.query_params.each { |k, v| params[k] = v.last }
    end
    
    multi_auth_user = multi_auth.user(user_params)

    unless user = Auth::User.get(email: multi_auth_user.email)
      user = Auth::User.create!(email: multi_auth_user.email) do |new_user|
        new_user.set_unusable_password
      end
    end

    MartenAuth.sign_in(request, user)

    redirect "/"
  end
end

Development

Install docker

Setup everythings

make setup

Run specs

make t
make t c=spec/providers/twitter_spec.cr

Run code linter

make l

Contributors

multi_auth's People

Contributors

cyangle avatar drujensen avatar ellmetha avatar jadekharats avatar jwoertink avatar mikeeus avatar msa7 avatar russ avatar rwojsznis avatar stephendolan avatar veelenga avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  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

multi_auth's Issues

Missing hash key urls

In the google provider, there's a line:

json["urls"].as_a.each do |url|

I setup my scope authorization to just be "profile email", and it tanks on this line at the callback. Inspecting the json at that point only has profile and email info, but no mention of a urls key.

Missing has: names

Hi

I tried the example given in the github page.
I used it for a google auth process.

However when the redirect_uri is called, i get the following error;

Missing hash key: "names" (KeyError)
0x559ef99d459b: fetch at /opt/crystal/src/hash.cr 124:9
0x559ef99d446d: [] at /opt/crystal/src/hash.cr 60:5
0x559ef99d4ee8: [] at /opt/crystal/src/json/any.cr 100:15
0x559ef999f175: primary? at /home/rick/DevSpace/Crystal/Kamel/lib/multi_auth/src/multi_auth/providers/google.cr 68:5
0x559ef999f06d: primary at /home/rick/DevSpace/Crystal/Kamel/lib/multi_auth/src/multi_auth/providers/google.cr 62:15
0x559ef999e3dc: build_user at /home/rick/DevSpace/Crystal/Kamel/lib/multi_auth/src/multi_auth/providers/google.cr 81:12
0x559ef999e360: user at /home/rick/DevSpace/Crystal/Kamel/lib/multi_auth/src/multi_auth/providers/google.cr 59:5
0x559ef999adac: user at /home/rick/DevSpace/Crystal/Kamel/lib/multi_auth/src/multi_auth/engine.cr 23:5
0x559ef9867c6c: ~procProc(HTTP::Server::Context, (String | Nil)) at /home/rick/DevSpace/Crystal/Kamel/lib/kemal/src/kemal/dsl.cr 10:1
0x559ef9869b1c: ~proc4Proc(HTTP::Server::Context, String) at /home/rick/DevSpace/Crystal/Kamel/lib/kemal/src/kemal/dsl.cr 10:1
0x559ef998326d: process_request at /home/rick/DevSpace/Crystal/Kamel/lib/kemal/src/kemal/route_handler.cr 255:3
0x559ef9983096: call at /home/rick/DevSpace/Crystal/Kamel/lib/kemal/src/kemal/route_handler.cr 18:7
0x559ef99e5e86: call_next at /opt/crystal/src/http/server/handler.cr 24:7
0x559ef99e5859: call at /home/rick/DevSpace/Crystal/Kamel/lib/kemal/src/kemal/static_file_handler.cr 56:9
0x559ef99e496d: call_next at /opt/crystal/src/http/server/handler.cr 24:7
0x559ef99e4450: call at /home/rick/DevSpace/Crystal/Kamel/lib/kemal/src/kemal/common_exception_handler.cr 9:9
0x559ef99e39f3: call_next at /opt/crystal/src/http/server/handler.cr 24:7
0x559ef99e0601: call at /home/rick/DevSpace/Crystal/Kamel/lib/kemal/src/kemal/common_log_handler.cr 13:35
0x559ef99820e8: call_next at /opt/crystal/src/http/server/handler.cr 24:7
0x559ef9981ccf: call at /home/rick/DevSpace/Crystal/Kamel/lib/kemal/src/kemal/init_handler.cr 11:7
0x559ef99ed80b: process at /opt/crystal/src/http/server/request_processor.cr 39:11
0x559ef99ed119: process at /opt/crystal/src/http/server/request_processor.cr 16:3
0x559ef99e75e0: handle_client at /opt/crystal/src/http/server.cr 191:5
0x559ef986b293: ~procProc(Nil) at /home/rick/DevSpace/Crystal/Kamel/lib/kemal/src/kemal/dsl.cr 10:1
0x559ef98875ce: run at /opt/crystal/src/fiber.cr 255:3
0x559ef98628a6: ~proc2Proc(Fiber, (IO::FileDescriptor | Nil)) at /opt/crystal/src/concurrent.cr 61:3
0x0: ??? at ??

Error trying to persist access token

Hi there,

I'm new to crystal and trying to spin up an oauth client. I have it working, but I'm running into trouble persisting my access_token. I want to access the string property of the access_token string instance method and use it to pull the string out of the instance. But, because the User class's access_token property can be nil, crystal won't let me access the property access_token for the type (OAuth2::AccessToken | Nil)

Forgive me if this is a novice question.

Allow custom providers

Some time ago I forked multi_auth, wanting to add Spotify support. But then I realized that maybe it's better to just allow developer to add their own provider, without the need to fork the project.

The outline commit for that is here: katafrakt@78ab475 Of course, this still requires testing and documentation, at least. I can do that and prepare a pull request, but since it's a new feature, I wanted to discuss it first.

Missing JSON attribute: id with Twitter

TwUser.from_json(raw_json).tap do |user|

If the Twitter provider isn't setup properly, you won't get back a valid user json on this line. Since there's no built-in logging here, the only thin you know is of the missing id attribute.

I had to update the code in lib/ to debug what the error was from the raw_json

{"errors":[{"message":"You currently have Essential access which includes access to Twitter API v2 endpoints only. If you need access to this endpoint, you’ll need to apply for Elevated access via the Developer Portal. You can learn more here: https://developer.twitter.com/en/docs/twitter-api/getting-started/about-twitter-api#v2-access-leve","code":453}]}

Maybe this can just catch the error, and re-raise raw_json?

Getting error over vk.cr while not even using vk

Trying to implement GitHub authentication with Amber but I keep an error over VK this during multi_auth.authorize_uri.
This is my initialization code:

MultiAuth.config("github", GITHUB_CLIENT_ID, GITHUB_SECRET_KEY)
multi_auth = MultiAuth.make("github", "/callback") 
multi_auth.authorize_uri

This is the output error from trying to build with amber watch:

multi_auth = MultiAuth.make("github", "/callback").authorize_uri # initialize engine
                                                   ^~~~~~~~~~~~~

in lib/multi_auth/src/multi_auth/engine.cr:20: instantiating 'authorize_uri(Nil)'

  def authorize_uri(scope = nil)
  ^~~~~~~~~~~~~

in lib/multi_auth/src/multi_auth/engine.cr:21: instantiating 'MultiAuth::Provider+#authorize_uri(Nil)'

    provider.authorize_uri(scope)
             ^~~~~~~~~~~~~

in lib/multi_auth/src/multi_auth/providers/vk.cr:4: instantiating 'client()'

    client.get_authorize_uri(@scope)
    ^~~~~~

in lib/multi_auth/src/multi_auth/providers/vk.cr:96: no argument named 'auth_scheme'
Matches are:
 - OAuth2::Client.new(host : String, client_id : String, client_secret : String, port : Int32 | ::Nil = nil, scheme = "https", authorize_uri = "/oauth2/authorize", token_uri = "/oauth2/token", redirect_uri : String | ::Nil = nil)

    OAuth2::Client.new(

I followed all the relevant integration examples and not even referring to VK at any point.
Any ideas?

Google refresh tokens

My OAuth keys for google are now saying expired. When I search for a solution, they say we're supposed to be using refresh tokens. How do I use refresh tokens with this sign in? The user connects with google, then is MultiAuth supposed to fetch a fresh token and then I'm supposed to save that or something? I'm not understanding how that flow is supposed to work in this case.

Twitter provider

Are you interested in this? Would you accept such a pull request?

The problem is twitter uses OAuth 1.0 (not 2.0), but this does not violate shard name. But will need to change the interfaces though.

https://dev.twitter.com/oauth

Twitter auth still uses OAuth1

The twitter auth is a bit tricky. All of the current supported engines use OAuth2 except Twitter.

@consumer ||= OAuth::Consumer.new("api.twitter.com", key, secret)

The issue is that Twitter itself gives you several different secret keys to choose from:
Screenshot from 2022-01-25 14-23-31

If you use the Access Token and Secret, you'll get a NilAssertionError, but no information that actually tells you that you're plugging OAuth2 keys in to an OAuth1 provider. In this case, you need to use the "Consumer Keys".

I'm not really sure what the actionable item is here, but here's a few options:

  • Upgrade Twitter to OAuth 2
  • Add docs telling you which one of these to use (i.e. the Consumer Keys)
  • Rescue the nil assertion, and raise a better exception that tells you your keys are not valid OAuth1 credentials.

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.