├── .github
└── workflows
│ └── ci.yml
├── .gitignore
├── LICENSE
├── Makefile
├── README.md
├── docker-compose.yml
├── setup.md
├── shard.yml
├── spec
├── providers
│ ├── discord_spec.cr
│ ├── facebook_spec.cr
│ ├── github_spec.cr
│ ├── gitlab_spec.cr
│ ├── google_spec.cr
│ ├── restream_spec.cr
│ ├── twitter_spec.cr
│ └── vk_spec.cr
├── spec_helper.cr
└── support
│ ├── discord.json
│ ├── github.json
│ ├── gitlab.json
│ ├── google.json
│ ├── google_api_disabled.json
│ ├── google_without_names.json
│ └── restream.json
└── src
├── multi_auth.cr
└── multi_auth
├── engine.cr
├── provider.cr
├── providers
├── discord.cr
├── facebook.cr
├── github.cr
├── gitlab.cr
├── google.cr
├── restream.cr
├── twitter.cr
└── vk.cr
├── user.cr
└── version.cr
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 | pull_request:
8 | schedule:
9 | - cron: "0 3 * * MON"
10 | workflow_dispatch:
11 |
12 | jobs:
13 | LintAndTest:
14 | strategy:
15 | fail-fast: false
16 | matrix:
17 | crystal_version:
18 | - 0.35.1
19 | - 0.36.1
20 | - latest
21 | experimental: [false]
22 | include:
23 | - crystal_version: nightly
24 | experimental: true
25 |
26 | runs-on: ubuntu-latest
27 | continue-on-error: ${{ matrix.experimental }}
28 |
29 | steps:
30 | - uses: actions/checkout@v2
31 |
32 | - uses: oprypin/install-crystal@v1
33 | with:
34 | crystal: ${{matrix.crystal_version}}
35 |
36 | - name: Check format
37 | run: crystal tool format --check
38 |
39 | - name: Set up Crystal cache
40 | uses: actions/cache@v2
41 | id: crystal-cache
42 | with:
43 | path: |
44 | ~/.cache/crystal
45 | lib
46 | key: ${{ runner.os }}-crystal-${{ matrix.crystal_version }}-${{ hashFiles('**/shard.lock') }}
47 | restore-keys: |
48 | ${{ runner.os }}-crystal-${{ matrix.crystal_version }}
49 |
50 | - name: Install shards
51 | if: steps.crystal-cache.outputs.cache-hit != 'true'
52 | run: shards check || shards install --ignore-crystal-version
53 |
54 | - name: Run tests
55 | run: crystal spec
56 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /doc/
2 | /lib/
3 | /bin/
4 | /.shards/
5 |
6 | # Libraries don't need dependency lock
7 | # Dependencies will be locked in application that uses them
8 | /shard.lock
9 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2017
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | RUN := docker-compose run --rm
2 | RUN_APP := $(RUN) app
3 |
4 | help: ## This help
5 | @awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST)
6 |
7 | setup: ## setup app
8 | docker-compose pull
9 | docker-compose build --force-rm app
10 | $(RUN_APP) shards install
11 | l: ## linter
12 | $(RUN) -e MULTI_AUTH_ENV=test app bash -c "crystal tool format"
13 | t: ## tests
14 | $(RUN) -e MULTI_AUTH_ENV=test app bash -c "crystal spec $(c)"
15 | sh: ## shell into app container c="pwd"
16 | $(RUN_APP) $(or $(c),bash)
17 | c: ## run console.cr
18 | $(RUN_APP) crystal run src/console.cr
19 | update_dependency: ## update_dependency
20 | docker-compose build --force-rm --no-cache --pull
21 | $(RUN_APP) rm -rf /app/lib/*
22 | $(RUN_APP) shards update
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # MultiAuth
2 |
3 | 
4 |
5 | MultiAuth is a library that standardizes multi-provider authentication for web applications. Currently supported providers:
6 |
7 | - Github.com
8 | - Gitlab.com (or [own instance](https://github.com/msa7/multi_auth/blob/master/setup.md#gitlab))
9 | - Facebook.com
10 | - Vk.com
11 | - Google.com, [setup google](https://github.com/msa7/multi_auth/blob/master/setup.md#google)
12 | - Twitter.com
13 | - Restream.io
14 |
15 | ## Installation
16 |
17 | Add this to your application's `shard.yml`:
18 |
19 | ```yaml
20 | dependencies:
21 | multi_auth:
22 | github: msa7/multi_auth
23 | ```
24 |
25 | ## Usage
26 |
27 | ### MultiAuth public interface
28 |
29 | ```crystal
30 | require "multi_auth"
31 |
32 | MultiAuth.config("github", ENV['ID'], ENV['SECRET']) # configuration
33 |
34 | multi_auth = MultiAuth.make(provider, redirect_uri) # initialize engine
35 | multi_auth.authorize_uri # URL to provider authentication dialog
36 |
37 | # on http callback, like /multi_auth/github/callback
38 | user = multi_auth.user(params) # get signed in user
39 | ```
40 |
41 | 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](https://github.com/msa7/multi_auth/blob/master/src/multi_auth/user.cr). Supported providers [src/multi_auth/providers](https://github.com/msa7/multi_auth/blob/master/src/multi_auth/providers). I hope it easy to add new providers.
42 |
43 | ### [Kemal](http://kemalcr.com) integration example
44 |
45 | ```html
46 | Sign in with Github
47 | ```
48 |
49 | ```crystal
50 | MultiAuth.config("facebook", "facebookClientID", "facebookSecretKey")
51 | MultiAuth.config("google", "googleClientID", "googleSecretKey")
52 |
53 | def self.multi_auth(env)
54 | provider = env.params.url["provider"]
55 | redirect_uri = "#{Kemal.config.scheme}://#{env.request.host_with_port.as(String)}/multi_auth/#{provider}/callback"
56 | MultiAuth.make(provider, redirect_uri)
57 | end
58 |
59 | get "/multi_auth/:provider" do |env|
60 | env.redirect(multi_auth(env).authorize_uri)
61 | end
62 |
63 | get "/multi_auth/:provider/callback" do |env|
64 | user = multi_auth(env).user(env.params.query)
65 | p user.email
66 | user
67 | end
68 | ```
69 |
70 | ### [Lucky](https://github.com/luckyframework/lucky) integration example
71 |
72 | ```crystal
73 | # config/watch.yml
74 | host: myapp.lvh.me
75 | port: 5000
76 |
77 | # config/multi_auth_handler.cr
78 | require "multi_auth"
79 |
80 | class MultiAuthHandler
81 | MultiAuth.config("facebook", "facebookClientID", "facebookSecretKey")
82 | MultiAuth.config("google", "googleClientID", "googleSecretKey")
83 |
84 | def self.authorize_uri(provider : String)
85 | MultiAuth.make(provider, "#{Lucky::RouteHelper.settings.base_uri}/oauth/#{provider}/callback").authorize_uri(scope: "email")
86 | end
87 |
88 | def self.user(provider : String, params : Enumerable({String, String}))
89 | MultiAuth.make(provider, "#{Lucky::RouteHelper.settings.base_uri}/oauth/#{provider}/callback").user(params)
90 | end
91 | end
92 |
93 | # src/actions/oauth/handler.cr
94 | class OAuth::Handler < BrowserAction
95 | get "/oauth/:provider" do
96 | redirect to: MultiAuthHandler.authorize_uri(provider)
97 | end
98 | end
99 |
100 | # src/actions/oauth/handler/callback.cr
101 | class OAuth::Handler::Callback < BrowserAction
102 | get "/oauth/:provider/callback" do
103 | user = MultiAuthHandler.user(provider, request.query_params)
104 | text user.email.to_s
105 | end
106 | end
107 | ```
108 |
109 | ### [Amber](https://github.com/amberframework/amber) integration example
110 |
111 | ```crystal
112 | # config/initializers/multi_auth.cr
113 | require "multi_auth"
114 |
115 | MultiAuth.config("facebook", "facebookClientID", "facebookSecretKey")
116 | MultiAuth.config("google", "googleClientID", "googleSecretKey")
117 |
118 | # config/routes.cr
119 | routes :web do
120 | ...
121 | get "/multi_auth/:provider", MultiAuthController, :new
122 | get "/multi_auth/:provider/callback", MultiAuthController, :callback
123 | end
124 |
125 | # src/controllers/multi_auth_controller.cr
126 | class MultiAuthController < ApplicationController
127 | def new
128 | redirect_to multi_auth.authorize_uri(scope: "email")
129 | end
130 |
131 | def callback
132 | multi_auth_user = multi_auth.user(request.query_params)
133 |
134 | if user = User.find_by email: multi_auth_user.email
135 | login user
136 | else
137 | user = User.create!(
138 | first_name: multi_auth_user.first_name,
139 | last_name: multi_auth_user.last_name,
140 | email: multi_auth_user.email
141 | )
142 | login user
143 | end
144 |
145 | redirect_to "/"
146 | end
147 |
148 | def login(user)
149 | context.session["user_id"] = user.id
150 | end
151 |
152 | def provider
153 | params[:provider]
154 | end
155 |
156 | def redirect_uri
157 | "#{Amber.settings.secrets["base_url"]}/multi_auth/#{provider}/callback"
158 | end
159 |
160 | def multi_auth
161 | MultiAuth.make(provider, redirect_uri)
162 | end
163 | end
164 | ```
165 |
166 | ### [Marten](https://github.com/martenframework/marten) integration example
167 |
168 | ```crystal
169 | # config/initializers/multi_auth.cr
170 | # ----
171 |
172 | require "multi_auth"
173 |
174 | MultiAuth.config("github", "", "")
175 |
176 |
177 | # config/routes.cr
178 | # ----
179 |
180 | Marten.routes.draw do
181 | path "/oauth/", OAuthInitiateHandler, name: "oauth_initiate"
182 | path "/oauth//callback", OAuthCallbackHandler, name: "oauth_callback"
183 | end
184 |
185 |
186 | # src/handlers/concerns/with_oauth.cr
187 | # ----
188 |
189 | module WithOAuth
190 | def multi_auth
191 | MultiAuth.make(provider, redirect_uri)
192 | end
193 |
194 | private def provider
195 | params["provider"].to_s
196 | end
197 |
198 | private def redirect_uri
199 | "#{request.scheme}://#{request.host}#{reverse("oauth_callback", provider: provider)}"
200 | end
201 | end
202 |
203 |
204 | # src/handlers/oauth_initiate_handler.cr
205 | # ----
206 |
207 | require "./concerns/**"
208 |
209 | class OAuthInitiateHandler < Marten::Handler
210 | include WithOAuth
211 |
212 | def get
213 | redirect multi_auth.authorize_uri(scope: "email")
214 | end
215 | end
216 |
217 |
218 | # src/handlers/oauth_initiate_callback.cr
219 | # ----
220 |
221 | require "./concerns/**"
222 |
223 | class OAuthCallbackHandler < Marten::Handler
224 | include WithOAuth
225 |
226 | def get
227 | user_params = Hash(String, String).new.tap do |params|
228 | request.query_params.each { |k, v| params[k] = v.last }
229 | end
230 |
231 | multi_auth_user = multi_auth.user(user_params)
232 |
233 | unless user = Auth::User.get(email: multi_auth_user.email)
234 | user = Auth::User.create!(email: multi_auth_user.email) do |new_user|
235 | new_user.set_unusable_password
236 | end
237 | end
238 |
239 | MartenAuth.sign_in(request, user)
240 |
241 | redirect "/"
242 | end
243 | end
244 | ```
245 |
246 | ## Development
247 |
248 | Install docker
249 |
250 | Setup everythings
251 |
252 | ```
253 | make setup
254 | ```
255 |
256 | Run specs
257 |
258 | ```
259 | make t
260 | make t c=spec/providers/twitter_spec.cr
261 | ```
262 |
263 | Run code linter
264 |
265 | ```
266 | make l
267 | ```
268 |
269 | ## Contributors
270 |
271 | - [Sergey Makridenkov](https://github.com/msa7) - creator, maintainer
272 | - [Vitalii Elenhaupt](https://github.com/veelenga) - contributor
273 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 |
3 | services:
4 | app:
5 | image: crystallang/crystal:latest
6 | working_dir: /app
7 | environment:
8 | EDA_ENV: ${MULTI_AUTH_ENV}
9 | volumes:
10 | - shards:/app/lib
11 | - .:/app
12 | volumes:
13 | shards:
14 |
--------------------------------------------------------------------------------
/setup.md:
--------------------------------------------------------------------------------
1 | # Google
2 |
3 | - Register app at [console](https://console.developers.google.com)
4 | - Generate OAuth [credentials](https://console.developers.google.com/apis/credentials). Use `http://localhost:8080/multi_auth/google/callback` as redirect URIs
5 | - Enable [Google People API](https://console.developers.google.com/apis/api/people.googleapis.com/overview?project=test-multi-auth)
6 |
7 | # Gitlab
8 |
9 | For own instance :
10 |
11 | - set OAUTH_GITLAB_URI environment variable
12 |
--------------------------------------------------------------------------------
/shard.yml:
--------------------------------------------------------------------------------
1 | name: multi_auth
2 | version: 1.1.1
3 |
4 | authors:
5 | - Sergey Makridenkov
6 |
7 | crystal: ">= 0.35.1, < 2.0.0"
8 |
9 | license: MIT
10 |
11 | development_dependencies:
12 | webmock:
13 | github: manastech/webmock.cr
14 | version: ~> 0.14
15 |
--------------------------------------------------------------------------------
/spec/providers/discord_spec.cr:
--------------------------------------------------------------------------------
1 | require "../spec_helper"
2 |
3 | describe MultiAuth::Provider::Discord do
4 | it "generates authorize_uri" do
5 | uri = MultiAuth.make("discord", "/callback").authorize_uri
6 | uri.should start_with("https://discord.com/api/oauth2/authorize?client_id=discord_id&redirect_uri=%2Fcallback&response_type=code&scope=identify")
7 | end
8 |
9 | it "generates authorize_uri with state query param" do
10 | uri = MultiAuth.make("discord", "/callback").authorize_uri(state: "random_state_value")
11 | uri.should start_with("https://discord.com/api/oauth2/authorize?client_id=discord_id&redirect_uri=%2Fcallback&response_type=code&scope=identify&state=random_state_value")
12 | end
13 |
14 | it "fetch user" do
15 | WebMock.wrap do
16 | WebMock.stub(:post, "https://discord.com/api/v8/oauth2/token")
17 | .with(
18 | body: "client_id=discord_id&client_secret=discord_secret&redirect_uri=%2Fcallback&grant_type=authorization_code&code=123",
19 | headers: {
20 | "Accept" => "application/json",
21 | "Content-type" => "application/x-www-form-urlencoded",
22 | }
23 | )
24 | .to_return(
25 | body: %({
26 | "access_token": "6qrZcUqja7812RVdnEKjpzOL4CvHBFG",
27 | "token_type": "Bearer",
28 | "expires_in": 604800,
29 | "refresh_token": "D43f5y0ahjqew82jZ4NViEr2YafMKhue",
30 | "scope": "identify"
31 | })
32 | )
33 |
34 | WebMock.stub(:get, "https://discord.com/api/v8/oauth2/@me")
35 | .to_return(body: File.read("spec/support/discord.json"))
36 |
37 | user = MultiAuth.make("discord", "/callback").user({"code" => "123"}).as(MultiAuth::User)
38 |
39 | user.name.should eq("Discord")
40 | end
41 | end
42 | end
43 |
--------------------------------------------------------------------------------
/spec/providers/facebook_spec.cr:
--------------------------------------------------------------------------------
1 | require "../spec_helper"
2 |
3 | describe MultiAuth::Provider::Facebook do
4 | it "generates authorize_uri" do
5 | uri = MultiAuth.make("facebook", "/callback").authorize_uri
6 | uri.should eq("https://www.facebook.com/v2.9/dialog/oauth?client_id=facebook_id&redirect_uri=%2Fcallback&response_type=code&scope=email")
7 | end
8 |
9 | it "generates authorize_uri with state query param" do
10 | uri = MultiAuth.make("facebook", "/callback").authorize_uri(state: "random_state_value")
11 | uri.should eq("https://www.facebook.com/v2.9/dialog/oauth?client_id=facebook_id&redirect_uri=%2Fcallback&response_type=code&scope=email&state=random_state_value")
12 | end
13 |
14 | it "fetch user" do
15 | WebMock.wrap do
16 | WebMock
17 | .stub(:post, "https://graph.facebook.com/v2.9/oauth/access_token")
18 | .with(
19 | body: "client_id=facebook_id&client_secret=facebook_secret&redirect_uri=%2Fcallback&grant_type=authorization_code&code=123",
20 | headers: {"Accept" => "application/json", "Content-type" => "application/x-www-form-urlencoded"})
21 | .to_return(
22 | body: %({
23 | "access_token" : "1111",
24 | "token_type" : "Bearer",
25 | "expires_in" : 899,
26 | "refresh_token" : null,
27 | "scope" : "user"
28 | })
29 | )
30 |
31 | WebMock
32 | .stub(:get, "https://graph.facebook.com/v2.9/me?fields=id,name,last_name,first_name,email,location,about,website")
33 | .to_return(
34 | body: %({
35 | "name" : "Sergey",
36 | "id" : "3333"
37 | })
38 | )
39 |
40 | user = MultiAuth.make("facebook", "/callback").user({"code" => "123"}).as(MultiAuth::User)
41 |
42 | user.name.should eq("Sergey")
43 | user.uid.should eq("3333")
44 | end
45 | end
46 | end
47 |
--------------------------------------------------------------------------------
/spec/providers/github_spec.cr:
--------------------------------------------------------------------------------
1 | require "../spec_helper"
2 |
3 | describe MultiAuth::Provider::Github do
4 | it "generates authorize_uri" do
5 | uri = MultiAuth.make("github", "/callback").authorize_uri
6 | uri.should eq("https://github.com/login/oauth/authorize?client_id=github_id&redirect_uri=&response_type=code&scope=user%3Aemail")
7 | end
8 |
9 | it "generates authorize_uri with state query param" do
10 | uri = MultiAuth.make("github", "/callback").authorize_uri(state: "random_state_value")
11 | uri.should eq("https://github.com/login/oauth/authorize?client_id=github_id&redirect_uri=&response_type=code&scope=user%3Aemail&state=random_state_value")
12 | end
13 |
14 | it "fetch user" do
15 | WebMock.wrap do
16 | WebMock.stub(:post, "https://github.com/login/oauth/access_token")
17 | .with(
18 | body: "client_id=github_id&client_secret=github_secret&redirect_uri=&grant_type=authorization_code&code=123",
19 | headers: {"Accept" => "application/json", "Content-type" => "application/x-www-form-urlencoded"}
20 | )
21 | .to_return(
22 | body: %({
23 | "access_token" : "1111",
24 | "token_type" : "Bearer",
25 | "expires_in" : 899,
26 | "refresh_token" : null,
27 | "scope" : "user"
28 | })
29 | )
30 |
31 | WebMock.stub(:get, "https://api.github.com/user")
32 | .to_return(body: File.read("spec/support/github.json"))
33 |
34 | user = MultiAuth.make("github", "/callback").user({"code" => "123"}).as(MultiAuth::User)
35 |
36 | user.email.should eq("hi@msa7.ru")
37 | end
38 | end
39 | end
40 |
--------------------------------------------------------------------------------
/spec/providers/gitlab_spec.cr:
--------------------------------------------------------------------------------
1 | require "../spec_helper"
2 |
3 | describe MultiAuth::Provider::Gitlab do
4 | it "generates authorize_uri" do
5 | uri = MultiAuth.make("gitlab", "/callback").authorize_uri
6 | uri.should eq("https://gitlab.com/oauth/authorize?client_id=gitlab_id&redirect_uri=%2Fcallback&response_type=code&scope=")
7 | end
8 |
9 | it "generates authorize_uri with state query param" do
10 | uri = MultiAuth.make("gitlab", "/callback").authorize_uri(state: "random_state_value")
11 | uri.should eq("https://gitlab.com/oauth/authorize?client_id=gitlab_id&redirect_uri=%2Fcallback&response_type=code&scope=&state=random_state_value")
12 | end
13 |
14 | it "fetch user" do
15 | WebMock.wrap do
16 | WebMock.stub(:post, "https://gitlab.com/oauth/token")
17 | .with(
18 | body: "client_id=gitlab_id&client_secret=gitlab_secret&redirect_uri=%2Fcallback&grant_type=authorization_code&code=123",
19 | headers: {"Accept" => "application/json", "Content-type" => "application/x-www-form-urlencoded"}
20 | )
21 | .to_return(
22 | body: %({
23 | "access_token" : "1111",
24 | "token_type" : "Bearer",
25 | "expires_in" : 899,
26 | "refresh_token" : null,
27 | "scope" : "user"
28 | })
29 | )
30 |
31 | WebMock.stub(:get, "https://gitlab.com/api/v4/user")
32 | .to_return(body: File.read("spec/support/gitlab.json"))
33 |
34 | user = MultiAuth.make("gitlab", "/callback").user({"code" => "123"}).as(MultiAuth::User)
35 |
36 | user.email.should eq("jade.kharats@gmail.com")
37 | end
38 | end
39 | end
40 |
--------------------------------------------------------------------------------
/spec/providers/google_spec.cr:
--------------------------------------------------------------------------------
1 | require "../spec_helper"
2 |
3 | describe MultiAuth::Provider::Google do
4 | it "generates authorize_uri" do
5 | uri = MultiAuth.make("google", "/callback").authorize_uri
6 | uri.should eq("https://accounts.google.com/o/oauth2/v2/auth?client_id=google_id&redirect_uri=%2Fcallback&response_type=code&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuser.emails.read+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuser.phonenumbers.read+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuser.addresses.read+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fplus.login+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fcontacts.readonly")
7 | end
8 |
9 | it "generates authorize_uri with state query param" do
10 | uri = MultiAuth.make("google", "/callback").authorize_uri(state: "random_state_value")
11 | uri.should eq("https://accounts.google.com/o/oauth2/v2/auth?client_id=google_id&redirect_uri=%2Fcallback&response_type=code&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuser.emails.read+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuser.phonenumbers.read+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuser.addresses.read+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fplus.login+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fcontacts.readonly&state=random_state_value")
12 | end
13 |
14 | it "fetch user" do
15 | WebMock.wrap do
16 | WebMock.stub(:post, "https://www.googleapis.com/oauth2/v4/token")
17 | .with(
18 | body: "client_id=google_id&client_secret=google_secret&redirect_uri=%2Fcallback&grant_type=authorization_code&code=123",
19 | headers: {"Accept" => "application/json", "Content-Length" => "111", "Host" => "www.googleapis.com", "Content-type" => "application/x-www-form-urlencoded"}
20 | )
21 | .to_return(
22 | body: %({
23 | "access_token" : "1111",
24 | "token_type" : "Bearer",
25 | "expires_in" : 899,
26 | "refresh_token" : null,
27 | "scope" : "user"
28 | })
29 | )
30 |
31 | WebMock.stub(:get, "https://people.googleapis.com/v1/people/me?personFields=addresses,biographies,bragging_rights,cover_photos,email_addresses,im_clients,interests,names,nicknames,phone_numbers,photos,urls")
32 | .to_return(body: File.read("spec/support/google.json"))
33 |
34 | user = MultiAuth.make("google", "/callback").user({"code" => "123"}).as(MultiAuth::User)
35 |
36 | user.email.should eq("smkrbr@gmail.com")
37 | end
38 | end
39 |
40 | context "when API disabled" do
41 | it "shows error" do
42 | WebMock.wrap do
43 | WebMock.stub(:post, "https://www.googleapis.com/oauth2/v4/token")
44 | .with(
45 | body: "client_id=google_id&client_secret=google_secret&redirect_uri=%2Fcallback&grant_type=authorization_code&code=123",
46 | headers: {"Accept" => "application/json", "Content-Length" => "111", "Host" => "www.googleapis.com", "Content-type" => "application/x-www-form-urlencoded"}
47 | )
48 | .to_return(
49 | body: %({
50 | "access_token" : "1111",
51 | "token_type" : "Bearer",
52 | "expires_in" : 899,
53 | "refresh_token" : null,
54 | "scope" : "user"
55 | })
56 | )
57 |
58 | WebMock.stub(:get, "https://people.googleapis.com/v1/people/me?personFields=addresses,biographies,bragging_rights,cover_photos,email_addresses,im_clients,interests,names,nicknames,phone_numbers,photos,urls")
59 | .to_return(body: File.read("spec/support/google_api_disabled.json"))
60 |
61 | expect_raises(Exception) do
62 | MultiAuth.make("google", "/callback").user({"code" => "123"})
63 | end
64 | end
65 | end
66 | end
67 |
68 | context "when the user has no names set on their account" do
69 | it "still returns a user" do
70 | WebMock.wrap do
71 | WebMock.stub(:post, "https://www.googleapis.com/oauth2/v4/token")
72 | .with(
73 | body: "client_id=google_id&client_secret=google_secret&redirect_uri=%2Fcallback&grant_type=authorization_code&code=123",
74 | headers: {"Accept" => "application/json", "Content-Length" => "111", "Host" => "www.googleapis.com", "Content-type" => "application/x-www-form-urlencoded"}
75 | )
76 | .to_return(
77 | body: %({
78 | "access_token" : "1111",
79 | "token_type" : "Bearer",
80 | "expires_in" : 899,
81 | "refresh_token" : null,
82 | "scope" : "user"
83 | })
84 | )
85 |
86 | WebMock.stub(:get, "https://people.googleapis.com/v1/people/me?personFields=addresses,biographies,bragging_rights,cover_photos,email_addresses,im_clients,interests,names,nicknames,phone_numbers,photos,urls")
87 | .to_return(body: File.read("spec/support/google_without_names.json"))
88 |
89 | user = MultiAuth.make("google", "/callback").user({"code" => "123"}).as(MultiAuth::User)
90 |
91 | user.email.should eq("smkrbr@gmail.com")
92 | end
93 | end
94 | end
95 | end
96 |
--------------------------------------------------------------------------------
/spec/providers/restream_spec.cr:
--------------------------------------------------------------------------------
1 | require "../spec_helper"
2 |
3 | describe MultiAuth::Provider::Restream do
4 | it "generates authorize_uri" do
5 | uri = MultiAuth.make("restream", "/callback").authorize_uri
6 | uri.should start_with("https://api.restream.io/login?client_id=restream_id&redirect_uri=%2Fcallback&response_type=code&state=")
7 | end
8 |
9 | it "generates authorize_uri with state query param" do
10 | uri = MultiAuth.make("restream", "/callback").authorize_uri(state: "random_state_value")
11 | uri.should start_with("https://api.restream.io/login?client_id=restream_id&redirect_uri=%2Fcallback&response_type=code&state=random_state_value")
12 | end
13 |
14 | it "fetch user" do
15 | WebMock.wrap do
16 | WebMock.stub(:post, "https://api.restream.io/oauth/token")
17 | .with(
18 | body: "redirect_uri=%2Fcallback&grant_type=authorization_code&code=123",
19 | headers: {
20 | "Accept" => "application/json",
21 | "Content-type" => "application/x-www-form-urlencoded",
22 | "Authorization" => "Basic cmVzdHJlYW1faWQ6cmVzdHJlYW1fc2VjcmV0",
23 | "Content-Length" => "63",
24 | "Host" => "api.restream.io",
25 | }
26 | )
27 | .to_return(
28 | body: %({
29 | "access_token": "7e61c8a5e2f99404730c511de6580412e618da35",
30 | "token_type" : "Bearer",
31 | "expires_in": 3600,
32 | "refresh_token": "0e633c3343a2df84b1526f4c2e6993ff17e05cab",
33 | "scopeJson" : [
34 | "profile.default.read",
35 | "stream.default.read"
36 | ]
37 | })
38 | )
39 |
40 | WebMock.stub(:get, "https://api.restream.io/v2/user/profile")
41 | .to_return(body: File.read("spec/support/restream.json"))
42 |
43 | user = MultiAuth.make("restream", "/callback").user({"code" => "123"}).as(MultiAuth::User)
44 |
45 | user.email.should eq("xxxxx@email.test")
46 | end
47 | end
48 | end
49 |
--------------------------------------------------------------------------------
/spec/providers/twitter_spec.cr:
--------------------------------------------------------------------------------
1 | require "../spec_helper"
2 |
3 | describe MultiAuth::Provider::Twitter do
4 | request_token_params = {
5 | oauth_token: "NPcudxy0yU5T3tBzho7iCotZ3cnetKwcTIRlX0iwRl0",
6 | oauth_token_secret: "veNRnAWe6inFuo8o2u8SLLZLjolYDmDP7SzL0YfYI",
7 | oauth_callback_confirmed: "true",
8 | }
9 |
10 | access_token_params = {
11 | oauth_token: "7588892-kagSNqWge8gB1WwE3plnFsJHAZVfxWD7Vb57p0b4",
12 | oauth_token_secret: "PbKfYqSryyeKDWz4ebtY3o5ogNLG11WJuZBc9fQrQo",
13 | }
14 |
15 | verify_credentials_params = {
16 | id: 38895958,
17 | name: "Sean Cook",
18 | screen_name: "theSeanCook",
19 | location: "San Francisco",
20 | url: "http://twitter.com",
21 | description: "I taught your phone that thing you like. The Mobile Partner Engineer @Twitter.",
22 | profile_image_url: "http://a0.twimg.com/profile_images/1751506047/dead_sexy_normal.JPG",
23 | email: "me@twitter.com",
24 | }
25 |
26 | describe "#authorize_uri" do
27 | it "generates authorize uri" do
28 | WebMock.allow_net_connect = true
29 | WebMock.stub(:post, "https://api.twitter.com/oauth/request_token")
30 | .to_return(body: HTTP::Params.encode request_token_params)
31 | uri = MultiAuth.make("twitter", "/callback").authorize_uri
32 | uri.should eq "https://api.twitter.com/oauth/authorize?oauth_token=NPcudxy0yU5T3tBzho7iCotZ3cnetKwcTIRlX0iwRl0&oauth_callback=%2Fcallback"
33 | end
34 | end
35 |
36 | describe "#fetch_tw_user" do
37 | it "successfully fetches user params" do
38 | WebMock.stub(:post, "https://api.twitter.com/oauth/access_token")
39 | .to_return(body: HTTP::Params.encode request_token_params)
40 |
41 | WebMock.stub(:get, "https://api.twitter.com/1.1/account/verify_credentials.json?include_email=true")
42 | .to_return(body: verify_credentials_params.to_json)
43 |
44 | user = MultiAuth.make("twitter", "/callback").user({"oauth_token" => "token", "oauth_verifier" => "verifier"})
45 |
46 | user.uid.should eq verify_credentials_params[:id].to_s
47 | user.email.should eq verify_credentials_params[:email]
48 | user.name.should eq verify_credentials_params[:name]
49 | user.nickname.should eq verify_credentials_params[:screen_name]
50 | user.location.should eq verify_credentials_params[:location]
51 | user.description.should eq verify_credentials_params[:description]
52 | user.image.should eq verify_credentials_params[:profile_image_url]
53 | user.urls.should eq({"twitter" => verify_credentials_params[:url]})
54 |
55 | user.provider.should eq "twitter"
56 | user.raw_json.should_not be_nil
57 | user.access_token.should_not be_nil
58 | end
59 | end
60 | end
61 |
--------------------------------------------------------------------------------
/spec/providers/vk_spec.cr:
--------------------------------------------------------------------------------
1 | require "../spec_helper"
2 |
3 | describe MultiAuth::Provider::Vk do
4 | it "generates authorize_uri" do
5 | uri = MultiAuth.make("vk", "/callback").authorize_uri
6 | uri.should eq("https://oauth.vk.com/authorize?client_id=vk_id&redirect_uri=%2Fcallback&response_type=code&scope=email")
7 | end
8 |
9 | it "generates authorize_uri with state query param" do
10 | uri = MultiAuth.make("vk", "/callback").authorize_uri(state: "random_state_value")
11 | uri.should eq("https://oauth.vk.com/authorize?client_id=vk_id&redirect_uri=%2Fcallback&response_type=code&scope=email&state=random_state_value")
12 | end
13 |
14 | it "fetch user" do
15 | WebMock.wrap do
16 | WebMock
17 | .stub(:post, "https://oauth.vk.com/access_token")
18 | .with(
19 | body: "client_id=vk_id&client_secret=vk_secret&redirect_uri=%2Fcallback&grant_type=authorization_code&code=123",
20 | headers: {"Accept" => "application/json", "Content-Length" => "103", "Host" => "oauth.vk.com", "Content-type" => "application/x-www-form-urlencoded"})
21 | .to_return(
22 | body: %({
23 | "access_token" : "1111",
24 | "expires_in" : 899,
25 | "refresh_token" : null,
26 | "scope" : "email",
27 | "user_id" : "3333",
28 | "email" : "s@msa7.ru"
29 | })
30 | )
31 |
32 | WebMock
33 | .stub(:get, %(https://api.vk.com/method/users.get?fields=about,photo_max_orig,city,country,domain,contacts,site&user_id="3333"&v=5.52))
34 | .to_return(
35 | body: %({"response": [{
36 | "first_name" : "Sergey",
37 | "last_name" : "Makridenkov",
38 | "id" : 3333
39 | }]})
40 | )
41 |
42 | user = MultiAuth.make("vk", "/callback").user({"code" => "123"}).as(MultiAuth::User)
43 |
44 | user.name.should eq("Makridenkov Sergey")
45 | user.uid.should eq("3333")
46 | end
47 | end
48 | end
49 |
--------------------------------------------------------------------------------
/spec/spec_helper.cr:
--------------------------------------------------------------------------------
1 | require "spec"
2 | require "webmock"
3 | require "../src/multi_auth"
4 |
5 | MultiAuth.config("google", "google_id", "google_secret")
6 | MultiAuth.config("github", "github_id", "github_secret")
7 | MultiAuth.config("gitlab", "gitlab_id", "gitlab_secret")
8 | MultiAuth.config("facebook", "facebook_id", "facebook_secret")
9 | MultiAuth.config("vk", "vk_id", "vk_secret")
10 | MultiAuth.config("twitter", "twitter_consumer_key", "twitter_consumer_secret")
11 | MultiAuth.config("restream", "restream_id", "restream_secret")
12 | MultiAuth.config("discord", "discord_id", "discord_secret")
13 |
--------------------------------------------------------------------------------
/spec/support/discord.json:
--------------------------------------------------------------------------------
1 | {
2 | "application": {
3 | "id": "159799960412356608",
4 | "name": "AIRHORN SOLUTIONS",
5 | "icon": "f03590d3eb764081d154a66340ea7d6d",
6 | "description": "",
7 | "summary": "",
8 | "hook": true,
9 | "bot_public": true,
10 | "bot_require_code_grant": false,
11 | "verify_key": "c8cde6a3c8c6e49d86af3191287b3ce255872be1fff6dc285bdb420c06a2c3c8"
12 | },
13 | "scopes": [
14 | "guilds.join",
15 | "identify"
16 | ],
17 | "expires": "2021-01-23T02:33:17.017000+00:00",
18 | "user": {
19 | "id": "268473310986240001",
20 | "username": "Discord",
21 | "avatar": "f749bb0cbeeb26ef21eca719337d20f1",
22 | "discriminator": "0001",
23 | "public_flags": 131072
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/spec/support/github.json:
--------------------------------------------------------------------------------
1 | {"login":"msa7","id":16959850,"avatar_url":"https://avatars1.githubusercontent.com/u/16959850?v=3","gravatar_id":"","url":"https://api.github.com/users/msa7","html_url":"https://github.com/msa7","followers_url":"https://api.github.com/users/msa7/followers","following_url":"https://api.github.com/users/msa7/following{/other_user}","gists_url":"https://api.github.com/users/msa7/gists{/gist_id}","starred_url":"https://api.github.com/users/msa7/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/msa7/subscriptions","organizations_url":"https://api.github.com/users/msa7/orgs","repos_url":"https://api.github.com/users/msa7/repos","events_url":"https://api.github.com/users/msa7/events{/privacy}","received_events_url":"https://api.github.com/users/msa7/received_events","type":"User","site_admin":false,"name":"Sergey Makridenkov","company":null,"blog":"msa7.ru","location":"Prague, CZ","email":"hi@msa7.ru","hireable":null,"bio":"Motto - quality and simplicity. My works are reliable and user friendly","public_repos":10,"public_gists":1,"followers":0,"following":25,"created_at":"2016-01-29T15:54:04Z","updated_at":"2017-04-11T08:35:26Z"}
2 |
--------------------------------------------------------------------------------
/spec/support/gitlab.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": 54,
3 | "name": "Jade D. Kharats",
4 | "username": "jade.kharats",
5 | "state": "active",
6 | "avatar_url": "https://secure.gravatar.com/avatar/90f6b53fcd57d203613bec94fa4dcd21?s=80\u0026d=identicon",
7 | "web_url": "https://gitlab.com/jade.kharats",
8 | "created_at": "2019-04-26T16:50:12.259+02:00",
9 | "bio": "",
10 | "bio_html": "",
11 | "location": null,
12 | "public_email": "",
13 | "skype": "",
14 | "linkedin": "",
15 | "twitter": "",
16 | "website_url": "",
17 | "organization": null,
18 | "job_title": "",
19 | "work_information": null,
20 | "last_sign_in_at": "2020-10-06T19:13:46.814+02:00",
21 | "confirmed_at": "2019-04-26T16:50:12.128+02:00",
22 | "last_activity_on": "2020-10-08",
23 | "email": "jade.kharats@gmail.com",
24 | "theme_id": 1,
25 | "color_scheme_id": 1,
26 | "projects_limit": 100000,
27 | "current_sign_in_at": "2020-10-08T21:53:56.028+02:00",
28 | "can_create_group": true,
29 | "can_create_project": true,
30 | "two_factor_enabled": true,
31 | "external": false,
32 | "private_profile": false
33 | }
34 |
--------------------------------------------------------------------------------
/spec/support/google.json:
--------------------------------------------------------------------------------
1 | {
2 | "resourceName": "people/107107280077356483059",
3 | "etag": "%EhIBEAUXGQkUIiUuAgoNDAsDGAY=",
4 | "metadata": {
5 | "sources": [
6 | {
7 | "type": "CONTACT",
8 | "id": "c8734cc8cdfacf3",
9 | "etag": "#TW9Lu5USlf4="
10 | },
11 | {
12 | "type": "CONTACT",
13 | "id": "15ea8fcc8a89f4de",
14 | "etag": "#qpol+eC6bLA="
15 | },
16 | {
17 | "type": "CONTACT",
18 | "id": "93",
19 | "etag": "#538/l64v70s="
20 | },
21 | {
22 | "type": "CONTACT",
23 | "id": "7f57a1b18d6a6aaa",
24 | "etag": "#PC96tiMIRRk="
25 | },
26 | {
27 | "type": "CONTACT",
28 | "id": "95",
29 | "etag": "#0NOQmzfvzLw="
30 | },
31 | {
32 | "type": "PROFILE",
33 | "id": "107107280077356483059",
34 | "etag": "#4eZz2/IuMFw=",
35 | "profileMetadata": {
36 | "objectType": "PERSON"
37 | }
38 | }
39 | ],
40 | "objectType": "PERSON"
41 | },
42 | "locales": [
43 | {
44 | "metadata": {
45 | "primary": true,
46 | "source": {
47 | "type": "ACCOUNT",
48 | "id": "107107280077356483059"
49 | }
50 | },
51 | "value": "en"
52 | }
53 | ],
54 | "names": [
55 | {
56 | "metadata": {
57 | "primary": true,
58 | "source": {
59 | "type": "PROFILE",
60 | "id": "107107280077356483059"
61 | }
62 | },
63 | "displayName": "Sergey Makridenkov",
64 | "familyName": "Makridenkov",
65 | "givenName": "Sergey",
66 | "displayNameLastFirst": "Makridenkov, Sergey"
67 | },
68 | {
69 | "metadata": {
70 | "source": {
71 | "type": "CONTACT",
72 | "id": "c8734cc8cdfacf3"
73 | }
74 | },
75 | "displayName": "Iam Я",
76 | "familyName": "Я",
77 | "givenName": "Iam",
78 | "displayNameLastFirst": "Я, Iam"
79 | },
80 | {
81 | "metadata": {
82 | "source": {
83 | "type": "CONTACT",
84 | "id": "15ea8fcc8a89f4de"
85 | }
86 | },
87 | "displayName": "Sergey Makridenkov",
88 | "familyName": "Makridenkov",
89 | "givenName": "Sergey",
90 | "displayNameLastFirst": "Makridenkov, Sergey"
91 | },
92 | {
93 | "metadata": {
94 | "source": {
95 | "type": "CONTACT",
96 | "id": "93"
97 | }
98 | },
99 | "displayName": "Sergey",
100 | "givenName": "Sergey",
101 | "displayNameLastFirst": "Sergey"
102 | }
103 | ],
104 | "nicknames": [
105 | {
106 | "metadata": {
107 | "primary": true,
108 | "source": {
109 | "type": "PROFILE",
110 | "id": "107107280077356483059"
111 | }
112 | },
113 | "value": "SergXIIIth"
114 | },
115 | {
116 | "metadata": {
117 | "source": {
118 | "type": "CONTACT",
119 | "id": "15ea8fcc8a89f4de"
120 | }
121 | },
122 | "value": "Sergey"
123 | },
124 | {
125 | "metadata": {
126 | "source": {
127 | "type": "CONTACT",
128 | "id": "93"
129 | }
130 | },
131 | "value": "Sergey"
132 | }
133 | ],
134 | "coverPhotos": [
135 | {
136 | "metadata": {
137 | "primary": true,
138 | "source": {
139 | "type": "PROFILE",
140 | "id": "107107280077356483059"
141 | }
142 | },
143 | "url": "https://lh3.googleusercontent.com/c5dqxl-2uHZ82ah9p7yxrVF1ZssrJNSV_15Nu0TUZwzCWqmtoLxCUJgEzLGtxsrJ6-v6R6rKU_-FYm881TTiMCJ_=s1600",
144 | "default": true
145 | }
146 | ],
147 | "photos": [
148 | {
149 | "metadata": {
150 | "primary": true,
151 | "source": {
152 | "type": "PROFILE",
153 | "id": "107107280077356483059"
154 | }
155 | },
156 | "url": "https://lh4.googleusercontent.com/-AD281PNjUXU/AAAAAAAAAAI/AAAAAAAALYQ/3WAUiy8jbHE/s100/photo.jpg"
157 | },
158 | {
159 | "metadata": {
160 | "source": {
161 | "type": "CONTACT",
162 | "id": "c8734cc8cdfacf3"
163 | }
164 | },
165 | "url": "https://lh4.googleusercontent.com/-CXdSX83lhoc/V-4T6R40_VI/AAAAAAAAAAA/T5JE4AgDpgU/s100/photo.jpg"
166 | },
167 | {
168 | "metadata": {
169 | "source": {
170 | "type": "CONTACT",
171 | "id": "15ea8fcc8a89f4de"
172 | }
173 | },
174 | "url": "https://lh5.googleusercontent.com/-0KHl0_KdiM4/V-4TSCrGNhI/AAAAAAAAAAA/8oI0NMAJz1c/s100/photo.jpg"
175 | },
176 | {
177 | "metadata": {
178 | "source": {
179 | "type": "CONTACT",
180 | "id": "93"
181 | }
182 | },
183 | "url": "https://lh3.googleusercontent.com/-ehsvcWTPv6Q/V-4TXpPyIRI/AAAAAAAAAAA/az30Nif1z_o/s100/photo.jpg"
184 | },
185 | {
186 | "metadata": {
187 | "source": {
188 | "type": "CONTACT",
189 | "id": "7f57a1b18d6a6aaa"
190 | }
191 | },
192 | "url": "https://lh6.googleusercontent.com/-xiYsuxbe4n4/V-4TRQn2AlI/AAAAAAAAAAA/Vl5kcSh4ajQ/s100/photo.jpg"
193 | },
194 | {
195 | "metadata": {
196 | "source": {
197 | "type": "CONTACT",
198 | "id": "95"
199 | }
200 | },
201 | "url": "https://lh4.googleusercontent.com/-DV151mfKFes/V-4TQJidLSI/AAAAAAAAAAA/YH54nPKjiyo/s100/photo.jpg"
202 | }
203 | ],
204 | "addresses": [
205 | {
206 | "metadata": {
207 | "primary": true,
208 | "source": {
209 | "type": "CONTACT",
210 | "id": "c8734cc8cdfacf3"
211 | }
212 | },
213 | "formattedValue": "Konětopy č.p. 17\n440 01 Pnětluky\nokres Louny\nkraj Ústecký",
214 | "type": "other",
215 | "formattedType": "Other",
216 | "streetAddress": "Konětopy č.p. 17",
217 | "city": "Pnětluky",
218 | "region": "okres Louny\nkraj Ústecký",
219 | "postalCode": "440 01"
220 | },
221 | {
222 | "metadata": {
223 | "source": {
224 | "type": "CONTACT",
225 | "id": "c8734cc8cdfacf3"
226 | }
227 | },
228 | "formattedValue": "187351\nРоссия, Ленинградская обл., Кировский район, село Путилово, ул. Братьев Пожарских, д.21, кв.10",
229 | "type": "home",
230 | "formattedType": "Home",
231 | "streetAddress": "187351\nРоссия, Ленинградская обл., Кировский район, село Путилово, ул. Братьев Пожарских, д.21, кв.10"
232 | }
233 | ],
234 | "emailAddresses": [
235 | {
236 | "metadata": {
237 | "primary": true,
238 | "verified": true,
239 | "source": {
240 | "type": "ACCOUNT",
241 | "id": "107107280077356483059"
242 | }
243 | },
244 | "value": "smkrbr@gmail.com"
245 | },
246 | {
247 | "metadata": {
248 | "verified": true,
249 | "source": {
250 | "type": "PROFILE",
251 | "id": "107107280077356483059"
252 | }
253 | },
254 | "value": "smkrbr@gmail.com",
255 | "type": "home",
256 | "formattedType": "Home"
257 | },
258 | {
259 | "metadata": {
260 | "verified": true,
261 | "source": {
262 | "type": "ACCOUNT",
263 | "id": "107107280077356483059"
264 | }
265 | },
266 | "value": "smkrbr@mail.ru"
267 | },
268 | {
269 | "metadata": {
270 | "verified": true,
271 | "source": {
272 | "type": "ACCOUNT",
273 | "id": "107107280077356483059"
274 | }
275 | },
276 | "value": "sergey@makridenkov.com"
277 | },
278 | {
279 | "metadata": {
280 | "source": {
281 | "type": "CONTACT",
282 | "id": "c8734cc8cdfacf3"
283 | }
284 | },
285 | "value": "smkrbr@gmail.com",
286 | "type": "home",
287 | "formattedType": "Home"
288 | },
289 | {
290 | "metadata": {
291 | "source": {
292 | "type": "CONTACT",
293 | "id": "c8734cc8cdfacf3"
294 | }
295 | },
296 | "value": "makridenkov@gmail.com",
297 | "type": "work",
298 | "formattedType": "Work"
299 | },
300 | {
301 | "metadata": {
302 | "source": {
303 | "type": "CONTACT",
304 | "id": "c8734cc8cdfacf3"
305 | }
306 | },
307 | "value": "sergey@makridenkov.com",
308 | "type": "work",
309 | "formattedType": "Work"
310 | },
311 | {
312 | "metadata": {
313 | "source": {
314 | "type": "CONTACT",
315 | "id": "c8734cc8cdfacf3"
316 | }
317 | },
318 | "value": "smkrbr@free.kindle.com",
319 | "type": "Kindle",
320 | "formattedType": "Kindle"
321 | },
322 | {
323 | "metadata": {
324 | "source": {
325 | "type": "CONTACT",
326 | "id": "c8734cc8cdfacf3"
327 | }
328 | },
329 | "value": "s@msa7.ru",
330 | "type": "other",
331 | "formattedType": "Other"
332 | },
333 | {
334 | "metadata": {
335 | "source": {
336 | "type": "CONTACT",
337 | "id": "15ea8fcc8a89f4de"
338 | }
339 | },
340 | "value": "smkrbr@gmail.com",
341 | "type": "home",
342 | "formattedType": "Home"
343 | },
344 | {
345 | "metadata": {
346 | "source": {
347 | "type": "CONTACT",
348 | "id": "93"
349 | }
350 | },
351 | "value": "smkrbr@gmail.com",
352 | "type": "other",
353 | "formattedType": "Other"
354 | },
355 | {
356 | "metadata": {
357 | "source": {
358 | "type": "CONTACT",
359 | "id": "7f57a1b18d6a6aaa"
360 | }
361 | },
362 | "value": "sergey@makridenkov.com"
363 | },
364 | {
365 | "metadata": {
366 | "source": {
367 | "type": "CONTACT",
368 | "id": "95"
369 | }
370 | },
371 | "value": "smkrbr@mail.ru"
372 | }
373 | ],
374 | "phoneNumbers": [
375 | {
376 | "metadata": {
377 | "primary": true,
378 | "source": {
379 | "type": "CONTACT",
380 | "id": "c8734cc8cdfacf3"
381 | }
382 | },
383 | "value": "+7 953 354-06-92",
384 | "canonicalForm": "+79533540692",
385 | "type": "work",
386 | "formattedType": "Work"
387 | },
388 | {
389 | "metadata": {
390 | "source": {
391 | "type": "CONTACT",
392 | "id": "c8734cc8cdfacf3"
393 | }
394 | },
395 | "value": "+420775357809",
396 | "canonicalForm": "+420775357809",
397 | "type": "mobile",
398 | "formattedType": "Mobile"
399 | },
400 | {
401 | "metadata": {
402 | "source": {
403 | "type": "CONTACT",
404 | "id": "c8734cc8cdfacf3"
405 | }
406 | },
407 | "value": "+79051621936",
408 | "canonicalForm": "+79051621936",
409 | "type": "home",
410 | "formattedType": "Home"
411 | },
412 | {
413 | "metadata": {
414 | "source": {
415 | "type": "PROFILE",
416 | "id": "107107280077356483059"
417 | }
418 | },
419 | "value": "+420774668079",
420 | "type": "mobile",
421 | "formattedType": "Mobile"
422 | }
423 | ],
424 | "biographies": [
425 | {
426 | "metadata": {
427 | "primary": true,
428 | "source": {
429 | "type": "CONTACT",
430 | "id": "c8734cc8cdfacf3"
431 | }
432 | },
433 | "value": "2600987645 / 2010 fio income\n\n\nmBank mKonto \nČíslo účtu 670100-2209502078/6210 \nČíslo účtu ve formátu IBAN CZ61 6210 6701 0022 0950 2078 \nČíslo BIC BREXCZPP \n\nIC 01311131\nDIC CZ8409272630",
434 | "contentType": "TEXT_PLAIN"
435 | }
436 | ],
437 | "urls": [
438 | {
439 | "metadata": {
440 | "primary": true,
441 | "verified": true,
442 | "source": {
443 | "type": "PROFILE",
444 | "id": "107107280077356483059"
445 | }
446 | },
447 | "value": "https://plus.google.com/107107280077356483059",
448 | "type": "profile",
449 | "formattedType": "Profile"
450 | }
451 | ],
452 | "organizations": [
453 | {
454 | "metadata": {
455 | "primary": true,
456 | "source": {
457 | "type": "PROFILE",
458 | "id": "107107280077356483059"
459 | }
460 | },
461 | "type": "work",
462 | "formattedType": "Work",
463 | "endDate": {
464 | "year": 2011,
465 | "month": 1,
466 | "day": 1
467 | },
468 | "current": true,
469 | "title": "Programmer, Web developer"
470 | }
471 | ],
472 | "occupations": [
473 | {
474 | "metadata": {
475 | "primary": true,
476 | "source": {
477 | "type": "PROFILE",
478 | "id": "107107280077356483059"
479 | }
480 | },
481 | "value": "Programmer, Web developer"
482 | }
483 | ]
484 | }
485 |
--------------------------------------------------------------------------------
/spec/support/google_api_disabled.json:
--------------------------------------------------------------------------------
1 | {
2 | "error": {
3 | "code": 403,
4 | "message": "Google People API has not been used in project test-multi-auth before or it is disabled. Enable it by visiting https://console.developers.google.com/apis/api/people.googleapis.com/overview?project=test-multi-auth then retry. If you enabled this API recently, wait a few minutes for the action to propagate to our systems and retry.",
5 | "status": "PERMISSION_DENIED",
6 | "details": [
7 | {
8 | "@type": "type.googleapis.com/google.rpc.Help",
9 | "links": [
10 | {
11 | "description": "Google developers console API activation",
12 | "url": "https://console.developers.google.com/apis/api/people.googleapis.com/overview?project=test-multi-auth"
13 | }
14 | ]
15 | }
16 | ]
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/spec/support/google_without_names.json:
--------------------------------------------------------------------------------
1 | {
2 | "resourceName": "people/107107280077356483059",
3 | "etag": "%EhIBEAUXGQkUIiUuAgoNDAsDGAY=",
4 | "metadata": {
5 | "sources": [
6 | {
7 | "type": "CONTACT",
8 | "id": "c8734cc8cdfacf3",
9 | "etag": "#TW9Lu5USlf4="
10 | },
11 | {
12 | "type": "CONTACT",
13 | "id": "15ea8fcc8a89f4de",
14 | "etag": "#qpol+eC6bLA="
15 | },
16 | {
17 | "type": "CONTACT",
18 | "id": "93",
19 | "etag": "#538/l64v70s="
20 | },
21 | {
22 | "type": "CONTACT",
23 | "id": "7f57a1b18d6a6aaa",
24 | "etag": "#PC96tiMIRRk="
25 | },
26 | {
27 | "type": "CONTACT",
28 | "id": "95",
29 | "etag": "#0NOQmzfvzLw="
30 | },
31 | {
32 | "type": "PROFILE",
33 | "id": "107107280077356483059",
34 | "etag": "#4eZz2/IuMFw=",
35 | "profileMetadata": {
36 | "objectType": "PERSON"
37 | }
38 | }
39 | ],
40 | "objectType": "PERSON"
41 | },
42 | "locales": [
43 | {
44 | "metadata": {
45 | "primary": true,
46 | "source": {
47 | "type": "ACCOUNT",
48 | "id": "107107280077356483059"
49 | }
50 | },
51 | "value": "en"
52 | }
53 | ],
54 | "names": [],
55 | "nicknames": [
56 | {
57 | "metadata": {
58 | "primary": true,
59 | "source": {
60 | "type": "PROFILE",
61 | "id": "107107280077356483059"
62 | }
63 | },
64 | "value": "SergXIIIth"
65 | },
66 | {
67 | "metadata": {
68 | "source": {
69 | "type": "CONTACT",
70 | "id": "15ea8fcc8a89f4de"
71 | }
72 | },
73 | "value": "Sergey"
74 | },
75 | {
76 | "metadata": {
77 | "source": {
78 | "type": "CONTACT",
79 | "id": "93"
80 | }
81 | },
82 | "value": "Sergey"
83 | }
84 | ],
85 | "coverPhotos": [
86 | {
87 | "metadata": {
88 | "primary": true,
89 | "source": {
90 | "type": "PROFILE",
91 | "id": "107107280077356483059"
92 | }
93 | },
94 | "url": "https://lh3.googleusercontent.com/c5dqxl-2uHZ82ah9p7yxrVF1ZssrJNSV_15Nu0TUZwzCWqmtoLxCUJgEzLGtxsrJ6-v6R6rKU_-FYm881TTiMCJ_=s1600",
95 | "default": true
96 | }
97 | ],
98 | "photos": [
99 | {
100 | "metadata": {
101 | "primary": true,
102 | "source": {
103 | "type": "PROFILE",
104 | "id": "107107280077356483059"
105 | }
106 | },
107 | "url": "https://lh4.googleusercontent.com/-AD281PNjUXU/AAAAAAAAAAI/AAAAAAAALYQ/3WAUiy8jbHE/s100/photo.jpg"
108 | },
109 | {
110 | "metadata": {
111 | "source": {
112 | "type": "CONTACT",
113 | "id": "c8734cc8cdfacf3"
114 | }
115 | },
116 | "url": "https://lh4.googleusercontent.com/-CXdSX83lhoc/V-4T6R40_VI/AAAAAAAAAAA/T5JE4AgDpgU/s100/photo.jpg"
117 | },
118 | {
119 | "metadata": {
120 | "source": {
121 | "type": "CONTACT",
122 | "id": "15ea8fcc8a89f4de"
123 | }
124 | },
125 | "url": "https://lh5.googleusercontent.com/-0KHl0_KdiM4/V-4TSCrGNhI/AAAAAAAAAAA/8oI0NMAJz1c/s100/photo.jpg"
126 | },
127 | {
128 | "metadata": {
129 | "source": {
130 | "type": "CONTACT",
131 | "id": "93"
132 | }
133 | },
134 | "url": "https://lh3.googleusercontent.com/-ehsvcWTPv6Q/V-4TXpPyIRI/AAAAAAAAAAA/az30Nif1z_o/s100/photo.jpg"
135 | },
136 | {
137 | "metadata": {
138 | "source": {
139 | "type": "CONTACT",
140 | "id": "7f57a1b18d6a6aaa"
141 | }
142 | },
143 | "url": "https://lh6.googleusercontent.com/-xiYsuxbe4n4/V-4TRQn2AlI/AAAAAAAAAAA/Vl5kcSh4ajQ/s100/photo.jpg"
144 | },
145 | {
146 | "metadata": {
147 | "source": {
148 | "type": "CONTACT",
149 | "id": "95"
150 | }
151 | },
152 | "url": "https://lh4.googleusercontent.com/-DV151mfKFes/V-4TQJidLSI/AAAAAAAAAAA/YH54nPKjiyo/s100/photo.jpg"
153 | }
154 | ],
155 | "addresses": [
156 | {
157 | "metadata": {
158 | "primary": true,
159 | "source": {
160 | "type": "CONTACT",
161 | "id": "c8734cc8cdfacf3"
162 | }
163 | },
164 | "formattedValue": "Konětopy č.p. 17\n440 01 Pnětluky\nokres Louny\nkraj Ústecký",
165 | "type": "other",
166 | "formattedType": "Other",
167 | "streetAddress": "Konětopy č.p. 17",
168 | "city": "Pnětluky",
169 | "region": "okres Louny\nkraj Ústecký",
170 | "postalCode": "440 01"
171 | },
172 | {
173 | "metadata": {
174 | "source": {
175 | "type": "CONTACT",
176 | "id": "c8734cc8cdfacf3"
177 | }
178 | },
179 | "formattedValue": "187351\nРоссия, Ленинградская обл., Кировский район, село Путилово, ул. Братьев Пожарских, д.21, кв.10",
180 | "type": "home",
181 | "formattedType": "Home",
182 | "streetAddress": "187351\nРоссия, Ленинградская обл., Кировский район, село Путилово, ул. Братьев Пожарских, д.21, кв.10"
183 | }
184 | ],
185 | "emailAddresses": [
186 | {
187 | "metadata": {
188 | "primary": true,
189 | "verified": true,
190 | "source": {
191 | "type": "ACCOUNT",
192 | "id": "107107280077356483059"
193 | }
194 | },
195 | "value": "smkrbr@gmail.com"
196 | },
197 | {
198 | "metadata": {
199 | "verified": true,
200 | "source": {
201 | "type": "PROFILE",
202 | "id": "107107280077356483059"
203 | }
204 | },
205 | "value": "smkrbr@gmail.com",
206 | "type": "home",
207 | "formattedType": "Home"
208 | },
209 | {
210 | "metadata": {
211 | "verified": true,
212 | "source": {
213 | "type": "ACCOUNT",
214 | "id": "107107280077356483059"
215 | }
216 | },
217 | "value": "smkrbr@mail.ru"
218 | },
219 | {
220 | "metadata": {
221 | "verified": true,
222 | "source": {
223 | "type": "ACCOUNT",
224 | "id": "107107280077356483059"
225 | }
226 | },
227 | "value": "sergey@makridenkov.com"
228 | },
229 | {
230 | "metadata": {
231 | "source": {
232 | "type": "CONTACT",
233 | "id": "c8734cc8cdfacf3"
234 | }
235 | },
236 | "value": "smkrbr@gmail.com",
237 | "type": "home",
238 | "formattedType": "Home"
239 | },
240 | {
241 | "metadata": {
242 | "source": {
243 | "type": "CONTACT",
244 | "id": "c8734cc8cdfacf3"
245 | }
246 | },
247 | "value": "makridenkov@gmail.com",
248 | "type": "work",
249 | "formattedType": "Work"
250 | },
251 | {
252 | "metadata": {
253 | "source": {
254 | "type": "CONTACT",
255 | "id": "c8734cc8cdfacf3"
256 | }
257 | },
258 | "value": "sergey@makridenkov.com",
259 | "type": "work",
260 | "formattedType": "Work"
261 | },
262 | {
263 | "metadata": {
264 | "source": {
265 | "type": "CONTACT",
266 | "id": "c8734cc8cdfacf3"
267 | }
268 | },
269 | "value": "smkrbr@free.kindle.com",
270 | "type": "Kindle",
271 | "formattedType": "Kindle"
272 | },
273 | {
274 | "metadata": {
275 | "source": {
276 | "type": "CONTACT",
277 | "id": "c8734cc8cdfacf3"
278 | }
279 | },
280 | "value": "s@msa7.ru",
281 | "type": "other",
282 | "formattedType": "Other"
283 | },
284 | {
285 | "metadata": {
286 | "source": {
287 | "type": "CONTACT",
288 | "id": "15ea8fcc8a89f4de"
289 | }
290 | },
291 | "value": "smkrbr@gmail.com",
292 | "type": "home",
293 | "formattedType": "Home"
294 | },
295 | {
296 | "metadata": {
297 | "source": {
298 | "type": "CONTACT",
299 | "id": "93"
300 | }
301 | },
302 | "value": "smkrbr@gmail.com",
303 | "type": "other",
304 | "formattedType": "Other"
305 | },
306 | {
307 | "metadata": {
308 | "source": {
309 | "type": "CONTACT",
310 | "id": "7f57a1b18d6a6aaa"
311 | }
312 | },
313 | "value": "sergey@makridenkov.com"
314 | },
315 | {
316 | "metadata": {
317 | "source": {
318 | "type": "CONTACT",
319 | "id": "95"
320 | }
321 | },
322 | "value": "smkrbr@mail.ru"
323 | }
324 | ],
325 | "phoneNumbers": [
326 | {
327 | "metadata": {
328 | "primary": true,
329 | "source": {
330 | "type": "CONTACT",
331 | "id": "c8734cc8cdfacf3"
332 | }
333 | },
334 | "value": "+7 953 354-06-92",
335 | "canonicalForm": "+79533540692",
336 | "type": "work",
337 | "formattedType": "Work"
338 | },
339 | {
340 | "metadata": {
341 | "source": {
342 | "type": "CONTACT",
343 | "id": "c8734cc8cdfacf3"
344 | }
345 | },
346 | "value": "+420775357809",
347 | "canonicalForm": "+420775357809",
348 | "type": "mobile",
349 | "formattedType": "Mobile"
350 | },
351 | {
352 | "metadata": {
353 | "source": {
354 | "type": "CONTACT",
355 | "id": "c8734cc8cdfacf3"
356 | }
357 | },
358 | "value": "+79051621936",
359 | "canonicalForm": "+79051621936",
360 | "type": "home",
361 | "formattedType": "Home"
362 | },
363 | {
364 | "metadata": {
365 | "source": {
366 | "type": "PROFILE",
367 | "id": "107107280077356483059"
368 | }
369 | },
370 | "value": "+420774668079",
371 | "type": "mobile",
372 | "formattedType": "Mobile"
373 | }
374 | ],
375 | "biographies": [
376 | {
377 | "metadata": {
378 | "primary": true,
379 | "source": {
380 | "type": "CONTACT",
381 | "id": "c8734cc8cdfacf3"
382 | }
383 | },
384 | "value": "2600987645 / 2010 fio income\n\n\nmBank mKonto \nČíslo účtu 670100-2209502078/6210 \nČíslo účtu ve formátu IBAN CZ61 6210 6701 0022 0950 2078 \nČíslo BIC BREXCZPP \n\nIC 01311131\nDIC CZ8409272630",
385 | "contentType": "TEXT_PLAIN"
386 | }
387 | ],
388 | "urls": [
389 | {
390 | "metadata": {
391 | "primary": true,
392 | "verified": true,
393 | "source": {
394 | "type": "PROFILE",
395 | "id": "107107280077356483059"
396 | }
397 | },
398 | "value": "https://plus.google.com/107107280077356483059",
399 | "type": "profile",
400 | "formattedType": "Profile"
401 | }
402 | ],
403 | "organizations": [
404 | {
405 | "metadata": {
406 | "primary": true,
407 | "source": {
408 | "type": "PROFILE",
409 | "id": "107107280077356483059"
410 | }
411 | },
412 | "type": "work",
413 | "formattedType": "Work",
414 | "endDate": {
415 | "year": 2011,
416 | "month": 1,
417 | "day": 1
418 | },
419 | "current": true,
420 | "title": "Programmer, Web developer"
421 | }
422 | ],
423 | "occupations": [
424 | {
425 | "metadata": {
426 | "primary": true,
427 | "source": {
428 | "type": "PROFILE",
429 | "id": "107107280077356483059"
430 | }
431 | },
432 | "value": "Programmer, Web developer"
433 | }
434 | ]
435 | }
436 |
--------------------------------------------------------------------------------
/spec/support/restream.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": 1000,
3 | "username": "xxxxx",
4 | "email": "xxxxx@email.test"
5 | }
6 |
--------------------------------------------------------------------------------
/src/multi_auth.cr:
--------------------------------------------------------------------------------
1 | require "oauth"
2 | require "oauth2"
3 | require "./multi_auth/**"
4 |
5 | module MultiAuth
6 | @@configuration = Hash(String, Array(String)).new
7 |
8 | def self.make(provider, redirect_uri)
9 | MultiAuth::Engine.new(provider, redirect_uri)
10 | end
11 |
12 | def self.configuration
13 | @@configuration
14 | end
15 |
16 | def self.config(provider, key, secret)
17 | @@configuration[provider] = [key, secret]
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/src/multi_auth/engine.cr:
--------------------------------------------------------------------------------
1 | class MultiAuth::Engine
2 | def initialize(provider : String, redirect_uri : String)
3 | provider_class = case provider
4 | when "google" then Provider::Google
5 | when "github" then Provider::Github
6 | when "gitlab" then Provider::Gitlab
7 | when "facebook" then Provider::Facebook
8 | when "vk" then Provider::Vk
9 | when "twitter" then Provider::Twitter
10 | when "restream" then Provider::Restream
11 | when "discord" then Provider::Discord
12 | else
13 | raise "Provider #{provider} not implemented"
14 | end
15 |
16 | key, secret = MultiAuth.configuration[provider]
17 | @provider = provider_class.new(redirect_uri, key, secret)
18 | end
19 |
20 | def initialize(@provider)
21 | end
22 |
23 | getter provider : Provider
24 |
25 | def authorize_uri(scope = nil, state = nil)
26 | provider.authorize_uri(scope, state)
27 | end
28 |
29 | def user(params : Enumerable({String, String})) : User
30 | provider.user(params.to_h)
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/src/multi_auth/provider.cr:
--------------------------------------------------------------------------------
1 | abstract class MultiAuth::Provider
2 | getter redirect_uri : String
3 | getter key : String
4 | getter secret : String
5 |
6 | abstract def authorize_uri(scope = nil, state = nil)
7 | abstract def user(params : Hash(String, String))
8 |
9 | def initialize(@redirect_uri : String, @key : String, @secret : String)
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/src/multi_auth/providers/discord.cr:
--------------------------------------------------------------------------------
1 | class MultiAuth::Provider::Discord < MultiAuth::Provider
2 | def authorize_uri(scope = nil, state = nil)
3 | defaults = [
4 | "identify",
5 | ]
6 |
7 | scope ||= defaults.join(" ")
8 |
9 | client = OAuth2::Client.new(
10 | "discord.com",
11 | key,
12 | secret,
13 | authorize_uri: "/api/oauth2/authorize",
14 | redirect_uri: redirect_uri
15 | )
16 |
17 | client.get_authorize_uri(scope, state)
18 | end
19 |
20 | private class DiscordUser
21 | include JSON::Serializable
22 |
23 | property raw_json : String?
24 | property access_token : OAuth::AccessToken?
25 |
26 | @[JSON::Field(converter: String::RawConverter)]
27 | property id : String
28 |
29 | property name : String
30 | property email : String?
31 | property avatar : String?
32 | end
33 |
34 | private def fetch_discord_user(oauth_token, oauth_verifier)
35 | request_token = OAuth::RequestToken.new(oauth_token, "")
36 | access_token = consumer.get_access_token(request_token, oauth_verifier)
37 |
38 | client = HTTP::Client.new("discord.com", tls: true)
39 | access_token.authenticate(client, key, secret)
40 |
41 | raw_json = client.get("/oauth2/authorize").body
42 |
43 | DiscordUser.from_json(raw_json).tap do |user|
44 | user.access_token = access_token
45 | user.raw_json = raw_json
46 | end
47 | end
48 |
49 | def user(params : Hash(String, String))
50 | client = OAuth2::Client.new(
51 | "discord.com",
52 | key,
53 | secret,
54 | token_uri: "/api/v8/oauth2/token",
55 | redirect_uri: redirect_uri,
56 | auth_scheme: :request_body
57 | )
58 |
59 | access_token = client.get_access_token_using_authorization_code(params["code"])
60 |
61 | api = HTTP::Client.new("discord.com", tls: true)
62 | access_token.authenticate(api)
63 |
64 | raw_json = api.get("/api/v8/oauth2/@me").body
65 |
66 | build_user(raw_json, access_token)
67 | end
68 |
69 | private def json
70 | @json.as(JSON::Any)
71 | end
72 |
73 | private def build_user(raw_json, access_token)
74 | @json = JSON.parse(raw_json)
75 |
76 | user = User.new(
77 | "discord",
78 | json["user"].as_h["id"].as_s,
79 | json["user"].as_h["username"].as_s,
80 | raw_json,
81 | access_token
82 | )
83 |
84 | if avatar = json["user"].as_h["avatar"]?
85 | user.image = "https://cdn.discordapp.com/avatars/#{json["user"].as_h["id"]}/#{avatar}"
86 | end
87 |
88 | user
89 | end
90 | end
91 |
--------------------------------------------------------------------------------
/src/multi_auth/providers/facebook.cr:
--------------------------------------------------------------------------------
1 | class MultiAuth::Provider::Facebook < MultiAuth::Provider
2 | def authorize_uri(scope = nil, state = nil)
3 | scope ||= "email"
4 | client.get_authorize_uri(scope, state)
5 | end
6 |
7 | def user(params : Hash(String, String))
8 | fb_user = fetch_fb_user(params["code"])
9 |
10 | user = User.new(
11 | "facebook",
12 | fb_user.id,
13 | fb_user.name,
14 | fb_user.raw_json.as(String),
15 | fb_user.access_token.not_nil!
16 | )
17 |
18 | user.email = fb_user.email
19 | user.first_name = fb_user.first_name
20 | user.last_name = fb_user.last_name
21 | user.location = fb_user.location
22 | user.description = fb_user.about
23 |
24 | urls = {} of String => String
25 | urls["web"] = fb_user.website.as(String) if fb_user.website
26 | user.urls = urls unless urls.empty?
27 |
28 | user
29 | end
30 |
31 | private class FbUser
32 | include JSON::Serializable
33 |
34 | property raw_json : String?
35 | property access_token : OAuth2::AccessToken?
36 | property picture_url : String?
37 |
38 | property id : String
39 | property name : String
40 | property last_name : String?
41 | property first_name : String?
42 | property email : String?
43 | property location : String?
44 | property about : String?
45 | property website : String?
46 | end
47 |
48 | private def fetch_fb_user(code)
49 | access_token = token_client.get_access_token_using_authorization_code(code)
50 | api = HTTP::Client.new("graph.facebook.com", tls: true)
51 | access_token.authenticate(api)
52 |
53 | raw_json = api.get("/v2.9/me?fields=id,name,last_name,first_name,email,location,about,website").body
54 |
55 | fb_user = FbUser.from_json(raw_json)
56 | fb_user.access_token = access_token
57 | fb_user.raw_json = raw_json
58 |
59 | fb_user
60 | end
61 |
62 | private def client
63 | OAuth2::Client.new(
64 | "www.facebook.com",
65 | key,
66 | secret,
67 | redirect_uri: redirect_uri,
68 | authorize_uri: "/v2.9/dialog/oauth",
69 | )
70 | end
71 |
72 | private def token_client
73 | OAuth2::Client.new(
74 | "graph.facebook.com",
75 | key,
76 | secret,
77 | redirect_uri: redirect_uri,
78 | token_uri: "/v2.9/oauth/access_token",
79 | auth_scheme: :request_body
80 | )
81 | end
82 | end
83 |
--------------------------------------------------------------------------------
/src/multi_auth/providers/github.cr:
--------------------------------------------------------------------------------
1 | class MultiAuth::Provider::Github < MultiAuth::Provider
2 | def authorize_uri(scope = nil, state = nil)
3 | scope ||= "user:email"
4 | client.get_authorize_uri(scope, state)
5 | end
6 |
7 | def user(params : Hash(String, String))
8 | gh_user = fetch_gh_user(params["code"])
9 |
10 | user = User.new(
11 | "github",
12 | gh_user.id,
13 | gh_user.name,
14 | gh_user.raw_json.as(String),
15 | gh_user.access_token.not_nil!
16 | )
17 |
18 | user.email = gh_user.email
19 | user.nickname = gh_user.login
20 | user.location = gh_user.location
21 | user.description = gh_user.bio
22 | user.image = gh_user.avatar_url
23 |
24 | urls = {} of String => String
25 | urls["blog"] = gh_user.blog.as(String) if gh_user.blog
26 | urls["github"] = gh_user.html_url.as(String) if gh_user.html_url
27 | user.urls = urls unless urls.empty?
28 |
29 | user
30 | end
31 |
32 | private class GhUser
33 | include JSON::Serializable
34 |
35 | property raw_json : String?
36 | property access_token : OAuth2::AccessToken?
37 |
38 | @[JSON::Field(converter: String::RawConverter)]
39 | property id : String
40 |
41 | property name : String?
42 | property email : String?
43 | property login : String
44 | property location : String?
45 | property bio : String?
46 | property avatar_url : String?
47 | property blog : String?
48 | property html_url : String?
49 | end
50 |
51 | private def fetch_gh_user(code)
52 | access_token = client.get_access_token_using_authorization_code(code)
53 |
54 | api = HTTP::Client.new("api.github.com", tls: true)
55 | access_token.authenticate(api)
56 |
57 | raw_json = api.get("/user").body
58 | gh_user = GhUser.from_json(raw_json)
59 | gh_user.access_token = access_token
60 | gh_user.raw_json = raw_json
61 | gh_user
62 | end
63 |
64 | private def client
65 | OAuth2::Client.new(
66 | "github.com",
67 | key,
68 | secret,
69 | authorize_uri: "/login/oauth/authorize",
70 | token_uri: "/login/oauth/access_token",
71 | auth_scheme: :request_body
72 | )
73 | end
74 | end
75 |
--------------------------------------------------------------------------------
/src/multi_auth/providers/gitlab.cr:
--------------------------------------------------------------------------------
1 | class MultiAuth::Provider::Gitlab < MultiAuth::Provider
2 | def authorize_uri(scope = nil, state = nil)
3 | scope ||= ""
4 | client.get_authorize_uri(scope, state)
5 | end
6 |
7 | def user(params : Hash(String, String))
8 | gitlab_user = fetch_gitlab_user(params["code"])
9 |
10 | user = User.new(
11 | "gitlab",
12 | gitlab_user.id,
13 | gitlab_user.name,
14 | gitlab_user.raw_json.as(String),
15 | gitlab_user.access_token.not_nil!
16 | )
17 |
18 | user.email = gitlab_user.email
19 | user.nickname = gitlab_user.username
20 | user.location = gitlab_user.location
21 | user.description = gitlab_user.bio
22 | user.image = gitlab_user.avatar_url
23 |
24 | urls = {} of String => String
25 | urls["gitlab"] = gitlab_user.web_url.as(String) if gitlab_user.web_url
26 |
27 | user
28 | end
29 |
30 | private class GitlabUser
31 | include JSON::Serializable
32 |
33 | property raw_json : String?
34 | property access_token : OAuth2::AccessToken?
35 |
36 | @[JSON::Field(converter: String::RawConverter)]
37 | property id : String
38 |
39 | property name : String
40 | property username : String
41 | property avatar_url : String?
42 | property web_url : String?
43 | property bio : String?
44 | property location : String?
45 | property email : String?
46 | end
47 |
48 | private def fetch_gitlab_user(code)
49 | access_token = client.get_access_token_using_authorization_code(code)
50 |
51 | api = HTTP::Client.new(gitlab_url, tls: true)
52 | access_token.authenticate(api)
53 |
54 | raw_json = api.get("/api/v4/user").body
55 | gitlab_user = GitlabUser.from_json(raw_json)
56 | gitlab_user.access_token = access_token
57 | gitlab_user.raw_json = raw_json
58 | gitlab_user
59 | end
60 |
61 | private def gitlab_url
62 | ENV["OAUTH_GITLAB_URI"]? || "gitlab.com"
63 | end
64 |
65 | private def client
66 | OAuth2::Client.new(
67 | gitlab_url,
68 | key,
69 | secret,
70 | authorize_uri: "/oauth/authorize",
71 | token_uri: "/oauth/token",
72 | redirect_uri: redirect_uri,
73 | auth_scheme: :request_body
74 | )
75 | end
76 | end
77 |
--------------------------------------------------------------------------------
/src/multi_auth/providers/google.cr:
--------------------------------------------------------------------------------
1 | class MultiAuth::Provider::Google < MultiAuth::Provider
2 | def authorize_uri(scope = nil, state = nil)
3 | defaults = [
4 | "https://www.googleapis.com/auth/user.emails.read",
5 | "https://www.googleapis.com/auth/user.phonenumbers.read",
6 | "https://www.googleapis.com/auth/user.addresses.read",
7 | "https://www.googleapis.com/auth/plus.login",
8 | "https://www.googleapis.com/auth/contacts.readonly",
9 | ]
10 |
11 | scope ||= defaults.join(" ")
12 |
13 | client = OAuth2::Client.new(
14 | "accounts.google.com",
15 | key,
16 | secret,
17 | authorize_uri: "/o/oauth2/v2/auth",
18 | redirect_uri: redirect_uri
19 | )
20 |
21 | client.get_authorize_uri(scope, state)
22 | end
23 |
24 | def user(params : Hash(String, String))
25 | client = OAuth2::Client.new(
26 | "www.googleapis.com",
27 | key,
28 | secret,
29 | token_uri: "/oauth2/v4/token",
30 | redirect_uri: redirect_uri,
31 | auth_scheme: :request_body
32 | )
33 |
34 | access_token = client.get_access_token_using_authorization_code(params["code"])
35 |
36 | # https://developers.google.com/people/api/rest/v1/people/get
37 | # enable Google People API
38 |
39 | api = HTTP::Client.new("people.googleapis.com", tls: true)
40 | access_token.authenticate(api)
41 |
42 | fields = [
43 | "addresses",
44 | "biographies",
45 | "bragging_rights",
46 | "cover_photos",
47 | "email_addresses",
48 | "im_clients",
49 | "interests",
50 | "names",
51 | "nicknames",
52 | "phone_numbers",
53 | "photos",
54 | "urls",
55 | ].join(",")
56 |
57 | raw_json = api.get("/v1/people/me?personFields=#{fields}").body
58 |
59 | build_user(raw_json, access_token)
60 | end
61 |
62 | private def primary(field)
63 | primary = primary?(field)
64 | raise "No primary in field #{field}" unless primary
65 | primary
66 | end
67 |
68 | private def primary?(field)
69 | field = json[field]?
70 | return unless field
71 |
72 | field.as_a.each do |item|
73 | return item if item["metadata"]["primary"].as_bool?
74 | end
75 |
76 | nil
77 | end
78 |
79 | private def json
80 | @json.as(JSON::Any)
81 | end
82 |
83 | private def build_user(raw_json, access_token)
84 | @json = JSON.parse(raw_json)
85 | raise json["error"]["message"].as_s if json["error"]?
86 |
87 | name = if primary?("names")
88 | primary("names")
89 | else
90 | JSON::Any.new({} of String => JSON::Any)
91 | end
92 |
93 | display_name = name["displayName"]?.to_s
94 |
95 | user = User.new(
96 | "google",
97 | json["resourceName"].as_s,
98 | display_name,
99 | raw_json,
100 | access_token
101 | )
102 |
103 | user.first_name = name["givenName"].as_s? if name["givenName"]?
104 | user.last_name = name["familyName"].as_s? if name["familyName"]?
105 |
106 | user.nickname = primary("nicknames")["value"].as_s if primary?("nicknames")
107 | user.image = primary("photos")["url"].as_s if primary?("photos")
108 | user.location = primary("addresses")["formattedValue"].as_s if primary?("addresses")
109 | user.email = primary("emailAddresses")["value"].as_s if primary?("emailAddresses")
110 | user.phone = primary("phoneNumbers")["canonicalForm"].as_s if primary?("phoneNumbers")
111 | user.description = primary("biographies")["value"].as_s if primary?("biographies")
112 |
113 | if json["urls"]?
114 | urls = {} of String => String
115 | json["urls"].as_a.each do |url|
116 | urls[url["type"].as_s] = url["value"].as_s
117 | end
118 | user.urls = urls
119 | end
120 |
121 | user
122 | end
123 | end
124 |
--------------------------------------------------------------------------------
/src/multi_auth/providers/restream.cr:
--------------------------------------------------------------------------------
1 | require "uuid"
2 |
3 | class MultiAuth::Provider::Restream < MultiAuth::Provider
4 | def authorize_uri(scope = nil, state = nil)
5 | state ||= UUID.random.to_s
6 | uri = client.get_authorize_uri(scope, state)
7 | end
8 |
9 | def user(params : Hash(String, String))
10 | rs_user = fetch_rs_user(params["code"])
11 |
12 | user = User.new(
13 | "restream",
14 | rs_user.id,
15 | rs_user.username,
16 | rs_user.raw_json.as(String),
17 | rs_user.access_token.not_nil!
18 | )
19 |
20 | user.email = rs_user.email
21 |
22 | user
23 | end
24 |
25 | private class RestreamUser
26 | include JSON::Serializable
27 |
28 | property raw_json : String?
29 | property access_token : OAuth2::AccessToken?
30 |
31 | @[JSON::Field(converter: String::RawConverter)]
32 | property id : String
33 |
34 | property username : String?
35 | property email : String?
36 | end
37 |
38 | private def fetch_rs_user(code)
39 | access_token = client.get_access_token_using_authorization_code(code)
40 |
41 | api = HTTP::Client.new("api.restream.io", tls: true)
42 | access_token.authenticate(api)
43 |
44 | raw_json = api.get("/v2/user/profile").body
45 | rs_user = RestreamUser.from_json(raw_json)
46 | rs_user.access_token = access_token
47 | rs_user.raw_json = raw_json
48 | rs_user
49 | end
50 |
51 | private def client
52 | OAuth2::Client.new(
53 | "api.restream.io",
54 | key,
55 | secret,
56 | authorize_uri: "/login",
57 | token_uri: "/oauth/token",
58 | redirect_uri: redirect_uri,
59 | auth_scheme: :http_basic
60 | )
61 | end
62 | end
63 |
--------------------------------------------------------------------------------
/src/multi_auth/providers/twitter.cr:
--------------------------------------------------------------------------------
1 | class MultiAuth::Provider::Twitter < MultiAuth::Provider
2 | def authorize_uri(scope = nil, state = nil)
3 | request_token = consumer.get_request_token(redirect_uri)
4 | consumer.get_authorize_uri(request_token, redirect_uri)
5 | end
6 |
7 | def user(params : Hash(String, String))
8 | tw_user = fetch_tw_user(params["oauth_token"], params["oauth_verifier"])
9 |
10 | user = User.new(
11 | "twitter",
12 | tw_user.id,
13 | tw_user.name,
14 | tw_user.raw_json.to_s,
15 | tw_user.access_token.not_nil!
16 | )
17 |
18 | user.email = tw_user.email
19 | user.nickname = tw_user.screen_name
20 | user.location = tw_user.location
21 | user.description = tw_user.description
22 | user.image = tw_user.profile_image_url
23 | if url = tw_user.url
24 | user.urls = {"twitter" => url}
25 | end
26 |
27 | user
28 | end
29 |
30 | private class TwUser
31 | include JSON::Serializable
32 |
33 | property raw_json : String?
34 | property access_token : OAuth::AccessToken?
35 |
36 | @[JSON::Field(converter: String::RawConverter)]
37 | property id : String
38 |
39 | property name : String
40 | property screen_name : String
41 | property location : String?
42 | property description : String?
43 | property url : String?
44 | property profile_image_url : String?
45 | property email : String?
46 | end
47 |
48 | private def fetch_tw_user(oauth_token, oauth_verifier)
49 | request_token = OAuth::RequestToken.new(oauth_token, "")
50 |
51 | access_token = consumer.get_access_token(request_token, oauth_verifier)
52 |
53 | client = HTTP::Client.new("api.twitter.com", tls: true)
54 | access_token.authenticate(client, key, secret)
55 |
56 | raw_json = client.get("/1.1/account/verify_credentials.json?include_email=true").body
57 |
58 | TwUser.from_json(raw_json).tap do |user|
59 | user.access_token = access_token
60 | user.raw_json = raw_json
61 | end
62 | end
63 |
64 | private def consumer
65 | @consumer ||= OAuth::Consumer.new("api.twitter.com", key, secret)
66 | end
67 | end
68 |
--------------------------------------------------------------------------------
/src/multi_auth/providers/vk.cr:
--------------------------------------------------------------------------------
1 | class MultiAuth::Provider::Vk < MultiAuth::Provider
2 | def authorize_uri(scope = nil, state = nil)
3 | scope ||= "email"
4 | client.get_authorize_uri(scope, state)
5 | end
6 |
7 | def user(params : Hash(String, String))
8 | vk_user = fetch_vk_user(params["code"])
9 |
10 | user = User.new(
11 | "vk",
12 | vk_user.id,
13 | vk_user.name,
14 | vk_user.raw_json.not_nil!,
15 | vk_user.access_token.not_nil!
16 | )
17 |
18 | user.email = vk_user.email
19 | user.first_name = vk_user.first_name
20 | user.last_name = vk_user.last_name
21 | user.nickname = vk_user.domain
22 | user.description = vk_user.about
23 | user.image = vk_user.photo_max_orig
24 | user.phone = vk_user.mobile_phone || vk_user.home_phone
25 |
26 | location = [] of String
27 | location << vk_user.city.not_nil!.title if vk_user.city
28 | location << vk_user.country.not_nil!.title if vk_user.country
29 | user.location = location.join(", ") unless location.empty?
30 |
31 | urls = {} of String => String
32 | urls["web"] = vk_user.site.not_nil! if vk_user.site
33 | user.urls = urls unless urls.empty?
34 |
35 | user
36 | end
37 |
38 | class VkTitle
39 | include JSON::Serializable
40 | property title : String
41 | end
42 |
43 | class VkUser
44 | include JSON::Serializable
45 |
46 | property raw_json : String?
47 | property access_token : OAuth2::AccessToken?
48 | property email : String?
49 | property id : String?
50 |
51 | def name
52 | "#{last_name} #{first_name}"
53 | end
54 |
55 | @[JSON::Field(converter: String::RawConverter)]
56 | property id : String
57 |
58 | property last_name : String?
59 | property first_name : String?
60 | property site : String?
61 | property city : VkTitle?
62 | property country : VkTitle?
63 | property domain : String?
64 | property about : String?
65 | property photo_max_orig : String?
66 | property mobile_phone : String?
67 | property home_phone : String?
68 | end
69 |
70 | class VkResponse
71 | include JSON::Serializable
72 | property response : Array(VkUser)
73 | end
74 |
75 | private def fetch_vk_user(code)
76 | access_token = client.get_access_token_using_authorization_code(code)
77 |
78 | api = HTTP::Client.new("api.vk.com", tls: true)
79 | access_token.authenticate(api)
80 |
81 | user_id = access_token.extra.not_nil!["user_id"]
82 | user_email = access_token.extra.not_nil!["email"]
83 |
84 | fields = "about,photo_max_orig,city,country,domain,contacts,site"
85 | raw_json = api.get("/method/users.get?fields=#{fields}&user_id=#{user_id}&v=5.52").body
86 |
87 | vk_user = VkResponse.from_json(raw_json).response.first
88 | vk_user.email = user_email
89 | vk_user.access_token = access_token
90 | vk_user.raw_json = raw_json
91 |
92 | vk_user
93 | end
94 |
95 | private def client
96 | OAuth2::Client.new(
97 | "oauth.vk.com",
98 | key,
99 | secret,
100 | redirect_uri: redirect_uri,
101 | authorize_uri: "/authorize",
102 | token_uri: "/access_token",
103 | auth_scheme: :request_body
104 | )
105 | end
106 | end
107 |
--------------------------------------------------------------------------------
/src/multi_auth/user.cr:
--------------------------------------------------------------------------------
1 | class MultiAuth::User
2 | def initialize(@provider, @uid, @name, @raw_json, @access_token)
3 | end
4 |
5 | getter provider : String
6 | getter uid : String
7 | getter name : String?
8 | getter raw_json : String
9 | getter access_token : OAuth::AccessToken | OAuth2::AccessToken
10 |
11 | property email : String?
12 | property nickname : String?
13 | property first_name : String?
14 | property last_name : String?
15 | property location : String?
16 | property description : String?
17 | property image : String?
18 | property phone : String?
19 | property urls : Hash(String, String)?
20 | end
21 |
--------------------------------------------------------------------------------
/src/multi_auth/version.cr:
--------------------------------------------------------------------------------
1 | module MultiAuth
2 | VERSION = "1.0.0"
3 | end
4 |
--------------------------------------------------------------------------------