authkeeper 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/README.md +28 -0
- data/Rakefile +10 -0
- data/app/assets/config/authkeeper_manifest.js +1 -0
- data/app/assets/stylesheets/authkeeper/application.css +15 -0
- data/app/controllers/authkeeper/omniauth_callbacks_controller.rb +68 -0
- data/app/helpers/authkeeper/application_helper.rb +38 -0
- data/app/jobs/authkeeper/application_job.rb +6 -0
- data/app/lib/authkeeper/github_api/client.rb +24 -0
- data/app/lib/authkeeper/github_api/requests/user.rb +16 -0
- data/app/lib/authkeeper/github_api/requests/user_emails.rb +16 -0
- data/app/lib/authkeeper/github_auth_api/client.rb +13 -0
- data/app/lib/authkeeper/github_auth_api/requests/fetch_access_token.rb +17 -0
- data/app/lib/authkeeper/gitlab_api/client.rb +21 -0
- data/app/lib/authkeeper/gitlab_api/requests/user.rb +16 -0
- data/app/lib/authkeeper/gitlab_auth_api/client.rb +13 -0
- data/app/lib/authkeeper/gitlab_auth_api/requests/fetch_access_token.rb +23 -0
- data/app/lib/authkeeper/google_api/client.rb +13 -0
- data/app/lib/authkeeper/google_api/requests/user.rb +13 -0
- data/app/lib/authkeeper/google_auth_api/client.rb +13 -0
- data/app/lib/authkeeper/google_auth_api/requests/fetch_access_token.rb +23 -0
- data/app/lib/authkeeper/jwt_encoder.rb +25 -0
- data/app/lib/authkeeper/yandex_api/client.rb +13 -0
- data/app/lib/authkeeper/yandex_api/requests/info.rb +16 -0
- data/app/lib/authkeeper/yandex_auth_api/client.rb +13 -0
- data/app/lib/authkeeper/yandex_auth_api/requests/fetch_access_token.rb +26 -0
- data/app/mailers/authkeeper/application_mailer.rb +8 -0
- data/app/models/authkeeper/application_record.rb +7 -0
- data/app/services/authkeeper/fetch_session_service.rb +29 -0
- data/app/services/authkeeper/generate_token_service.rb +17 -0
- data/app/services/authkeeper/providers/github.rb +54 -0
- data/app/services/authkeeper/providers/gitlab.rb +47 -0
- data/app/services/authkeeper/providers/google.rb +46 -0
- data/app/services/authkeeper/providers/telegram.rb +58 -0
- data/app/services/authkeeper/providers/yandex.rb +50 -0
- data/app/views/layouts/authkeeper/application.html.erb +12 -0
- data/config/routes.rb +6 -0
- data/lib/authkeeper/configuration.rb +32 -0
- data/lib/authkeeper/container.rb +40 -0
- data/lib/authkeeper/controllers/authentication.rb +54 -0
- data/lib/authkeeper/engine.rb +7 -0
- data/lib/authkeeper/version.rb +5 -0
- data/lib/authkeeper.rb +37 -0
- metadata +102 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 1d291c37e7026099622fefe84c63a02de6cef06daa7eee635f36b796db5c7aa9
|
4
|
+
data.tar.gz: 907a89191c1c28605281792a51e5152ff8d3a6772d2bd9b1b9123a4cce7a9d24
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: accb54c1f14af1647d1ec78ce89cb6497b68086bf413e14fbd0f84a157eb85756fd7bdbc7059fd2428b906f8581a36f5b6d8d2327b2ab6c73bd782197a2558e3
|
7
|
+
data.tar.gz: cafb801a5ad8b25a840d6bf499eca9b6f8593eac07b6b446fd6b565cb71ddd864c2e11bbdec959933fe48e2c6e0ead259cf844098677f1c4b80e935c334ae932
|
data/README.md
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# Authkeeper
|
2
|
+
Short description and motivation.
|
3
|
+
|
4
|
+
## Usage
|
5
|
+
How to use my plugin.
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
Add this line to your application's Gemfile:
|
9
|
+
|
10
|
+
```ruby
|
11
|
+
gem "authkeeper"
|
12
|
+
```
|
13
|
+
|
14
|
+
And then execute:
|
15
|
+
```bash
|
16
|
+
$ bundle
|
17
|
+
```
|
18
|
+
|
19
|
+
Or install it yourself as:
|
20
|
+
```bash
|
21
|
+
$ gem install authkeeper
|
22
|
+
```
|
23
|
+
|
24
|
+
## Contributing
|
25
|
+
Contribution directions go here.
|
26
|
+
|
27
|
+
## License
|
28
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
//= link_directory ../stylesheets/authkeeper .css
|
@@ -0,0 +1,15 @@
|
|
1
|
+
/*
|
2
|
+
* This is a manifest file that'll be compiled into application.css, which will include all the files
|
3
|
+
* listed below.
|
4
|
+
*
|
5
|
+
* Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
|
6
|
+
* or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
|
7
|
+
*
|
8
|
+
* You're free to add application-wide styles to this file and they'll appear at the bottom of the
|
9
|
+
* compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
|
10
|
+
* files in this directory. Styles in this file should be added after the last require_* statement.
|
11
|
+
* It is generally better to create a new file per style scope.
|
12
|
+
*
|
13
|
+
*= require_tree .
|
14
|
+
*= require_self
|
15
|
+
*/
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Authkeeper
|
4
|
+
class OmniauthCallbacksController < ::ApplicationController
|
5
|
+
include AuthkeeperDeps[
|
6
|
+
fetch_session: 'services.fetch_session',
|
7
|
+
generate_token: 'services.generate_token'
|
8
|
+
]
|
9
|
+
|
10
|
+
GITHUB = 'github'
|
11
|
+
GITLAB = 'gitlab'
|
12
|
+
TELEGRAM = 'telegram'
|
13
|
+
GOOGLE = 'google'
|
14
|
+
YANDEX = 'yandex'
|
15
|
+
|
16
|
+
skip_before_action :verify_authenticity_token
|
17
|
+
skip_before_action :authenticate, only: %i[create]
|
18
|
+
before_action :validate_provider, only: %i[create]
|
19
|
+
before_action :validate_auth, only: %i[create]
|
20
|
+
|
21
|
+
def create; end
|
22
|
+
|
23
|
+
def destroy
|
24
|
+
fetch_session
|
25
|
+
.call(token: cookies[Authkeeper.configuration.access_token_name])[:result]
|
26
|
+
&.destroy
|
27
|
+
cookies.delete(Authkeeper.configuration.access_token_name)
|
28
|
+
redirect_to root_path
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def validate_provider
|
34
|
+
authentication_error if Authkeeper.configuration.omniauth_providers.exclude?(params[:provider])
|
35
|
+
end
|
36
|
+
|
37
|
+
def validate_auth
|
38
|
+
return validate_telegram_auth if params[:provider] == TELEGRAM
|
39
|
+
|
40
|
+
validate_general_auth
|
41
|
+
end
|
42
|
+
|
43
|
+
def validate_general_auth
|
44
|
+
authentication_error if params[:code].blank? || auth.nil?
|
45
|
+
end
|
46
|
+
|
47
|
+
def validate_telegram_auth
|
48
|
+
authentication_error if params[:id].blank? || auth.nil?
|
49
|
+
end
|
50
|
+
|
51
|
+
def auth
|
52
|
+
@auth ||= provider_service(params[:provider]).call(params: params)[:result]
|
53
|
+
end
|
54
|
+
|
55
|
+
def provider_service(provider)
|
56
|
+
Authkeeper::Container.resolve("services.providers.#{provider}")
|
57
|
+
end
|
58
|
+
|
59
|
+
def sign_in(user)
|
60
|
+
user_session = Authkeeper.configuration.user_session_model.constantize.create!(user: user)
|
61
|
+
cookies[Authkeeper.configuration.access_token_name] = {
|
62
|
+
value: generate_token.call(user_session: user_session)[:result],
|
63
|
+
domain: Authkeeper.configuration.domain,
|
64
|
+
expires: 1.week.from_now
|
65
|
+
}.compact
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Authkeeper
|
4
|
+
module ApplicationHelper
|
5
|
+
def omniauth_link(provider)
|
6
|
+
case provider
|
7
|
+
when :github then github_oauth_link
|
8
|
+
when :gitlab then gitlab_oauth_link
|
9
|
+
when :google then google_oauth_link
|
10
|
+
when :yandex then yandex_oauth_link
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
# rubocop: disable Layout/LineLength
|
17
|
+
def github_oauth_link
|
18
|
+
"https://github.com/login/oauth/authorize?scope=user:email&response_type=code&client_id=#{value(:github, :client_id)}&redirect_uri=#{value(:github, :redirect_url)}"
|
19
|
+
end
|
20
|
+
|
21
|
+
def gitlab_oauth_link
|
22
|
+
"https://gitlab.com/oauth/authorize?scope=read_user&response_type=code&client_id=#{value(:gitlab, :client_id)}&redirect_uri=#{value(:gitlab, :redirect_url)}"
|
23
|
+
end
|
24
|
+
|
25
|
+
def google_oauth_link
|
26
|
+
"https://accounts.google.com/o/oauth2/auth?scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email&response_type=code&client_id=#{value(:google, :client_id)}&redirect_uri=#{value(:google, :redirect_url)}"
|
27
|
+
end
|
28
|
+
|
29
|
+
def yandex_oauth_link
|
30
|
+
"https://oauth.yandex.ru/authorize?response_type=code&client_id=#{value(:yandex, :client_id)}"
|
31
|
+
end
|
32
|
+
# rubocop: enable Layout/LineLength
|
33
|
+
|
34
|
+
def value(provider, key)
|
35
|
+
Authkeeper.configuration.omniauth_configs.dig(provider, key)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Authkeeper
|
4
|
+
module GithubApi
|
5
|
+
class Client < HttpService::Client
|
6
|
+
include Requests::User
|
7
|
+
include Requests::UserEmails
|
8
|
+
|
9
|
+
BASE_URL = 'https://api.github.com'
|
10
|
+
|
11
|
+
option :url, default: proc { BASE_URL }
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def headers(access_token)
|
16
|
+
{
|
17
|
+
'Accept' => 'application/vnd.github+json',
|
18
|
+
'X-GitHub-Api-Version' => '2022-11-28',
|
19
|
+
'Authorization' => "Bearer #{access_token}"
|
20
|
+
}
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Authkeeper
|
4
|
+
module GithubAuthApi
|
5
|
+
module Requests
|
6
|
+
module FetchAccessToken
|
7
|
+
def fetch_access_token(client_id:, client_secret:, code:)
|
8
|
+
post(
|
9
|
+
path: 'login/oauth/access_token',
|
10
|
+
params: { client_id: client_id, client_secret: client_secret, code: code },
|
11
|
+
headers: { 'Accept' => 'application/vnd.github+json', 'X-GitHub-Api-Version' => '2022-11-28' }
|
12
|
+
)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Authkeeper
|
4
|
+
module GitlabApi
|
5
|
+
class Client < HttpService::Client
|
6
|
+
include Requests::User
|
7
|
+
|
8
|
+
BASE_URL = 'https://gitlab.com'
|
9
|
+
|
10
|
+
option :url, default: proc { BASE_URL }
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def headers(access_token)
|
15
|
+
{
|
16
|
+
'Authorization' => "Bearer #{access_token}"
|
17
|
+
}
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Authkeeper
|
4
|
+
module GitlabAuthApi
|
5
|
+
module Requests
|
6
|
+
module FetchAccessToken
|
7
|
+
def fetch_access_token(client_id:, client_secret:, redirect_url:, code:)
|
8
|
+
post(
|
9
|
+
path: 'oauth/token',
|
10
|
+
params: {
|
11
|
+
client_id: client_id,
|
12
|
+
client_secret: client_secret,
|
13
|
+
redirect_uri: redirect_url,
|
14
|
+
code: code,
|
15
|
+
grant_type: 'authorization_code'
|
16
|
+
},
|
17
|
+
headers: { 'Content-type' => 'application/json' }
|
18
|
+
)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Authkeeper
|
4
|
+
module GoogleAuthApi
|
5
|
+
class Client < HttpService::Client
|
6
|
+
include Requests::FetchAccessToken
|
7
|
+
|
8
|
+
BASE_URL = 'https://www.googleapis.com/'
|
9
|
+
|
10
|
+
option :url, default: proc { BASE_URL }
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Authkeeper
|
4
|
+
module GoogleAuthApi
|
5
|
+
module Requests
|
6
|
+
module FetchAccessToken
|
7
|
+
def fetch_access_token(client_id:, client_secret:, code:, redirect_uri:)
|
8
|
+
post(
|
9
|
+
path: 'oauth2/v4/token',
|
10
|
+
params: {
|
11
|
+
grant_type: 'authorization_code',
|
12
|
+
client_id: client_id,
|
13
|
+
client_secret: client_secret,
|
14
|
+
code: code,
|
15
|
+
redirect_uri: redirect_uri
|
16
|
+
},
|
17
|
+
headers: { 'Content-Type' => 'application/x-www-form-urlencoded' }
|
18
|
+
)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Authkeeper
|
4
|
+
class JwtEncoder
|
5
|
+
HMAC_SECRET = Rails.application.secret_key_base
|
6
|
+
EXPIRATION_SECONDS = 604_800 # 1.week
|
7
|
+
|
8
|
+
def encode(payload:, secret: HMAC_SECRET)
|
9
|
+
JWT.encode(modify_payload(payload), secret)
|
10
|
+
end
|
11
|
+
|
12
|
+
def decode(token:, secret: HMAC_SECRET)
|
13
|
+
JWT.decode(token, secret).first
|
14
|
+
rescue JWT::DecodeError
|
15
|
+
{}
|
16
|
+
end
|
17
|
+
|
18
|
+
def modify_payload(payload)
|
19
|
+
payload.merge!(
|
20
|
+
random: SecureRandom.hex,
|
21
|
+
exp: DateTime.now.to_i + EXPIRATION_SECONDS
|
22
|
+
)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Authkeeper
|
4
|
+
module YandexApi
|
5
|
+
module Requests
|
6
|
+
module Info
|
7
|
+
def info(access_token:)
|
8
|
+
get(
|
9
|
+
path: 'info',
|
10
|
+
headers: { 'Authorization' => "OAuth #{access_token}" }
|
11
|
+
)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Authkeeper
|
4
|
+
module YandexAuthApi
|
5
|
+
class Client < HttpService::Client
|
6
|
+
include Requests::FetchAccessToken
|
7
|
+
|
8
|
+
BASE_URL = 'https://oauth.yandex.ru/'
|
9
|
+
|
10
|
+
option :url, default: proc { BASE_URL }
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'uri'
|
4
|
+
|
5
|
+
module Authkeeper
|
6
|
+
module YandexAuthApi
|
7
|
+
module Requests
|
8
|
+
module FetchAccessToken
|
9
|
+
def fetch_access_token(client_id:, client_secret:, code:)
|
10
|
+
post(
|
11
|
+
path: 'token',
|
12
|
+
body: URI.encode_www_form({
|
13
|
+
grant_type: 'authorization_code',
|
14
|
+
client_id: client_id,
|
15
|
+
client_secret: client_secret,
|
16
|
+
code: code
|
17
|
+
}),
|
18
|
+
headers: {
|
19
|
+
'Content-Type' => 'application/x-www-form-urlencoded'
|
20
|
+
}
|
21
|
+
)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Authkeeper
|
4
|
+
class FetchSessionService
|
5
|
+
include AuthkeeperDeps[jwt_encoder: 'jwt_encoder']
|
6
|
+
|
7
|
+
def call(token:)
|
8
|
+
payload = extract_uuid(token)
|
9
|
+
return { errors: ['Forbidden'] } if payload.blank?
|
10
|
+
|
11
|
+
session = find_session(payload)
|
12
|
+
return { errors: ['Forbidden'] } if session.blank?
|
13
|
+
|
14
|
+
{ result: session }
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def extract_uuid(token)
|
20
|
+
jwt_encoder.decode(token: token)
|
21
|
+
end
|
22
|
+
|
23
|
+
def find_session(payload)
|
24
|
+
Authkeeper
|
25
|
+
.configuration.user_session_model.constantize
|
26
|
+
.find_by(id: payload.fetch('uuid', ''))
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Authkeeper
|
4
|
+
class GenerateTokenService
|
5
|
+
include AuthkeeperDeps[jwt_encoder: 'jwt_encoder']
|
6
|
+
|
7
|
+
def call(user_session:)
|
8
|
+
{
|
9
|
+
result: jwt_encoder.encode(
|
10
|
+
payload: {
|
11
|
+
uuid: user_session.id
|
12
|
+
}
|
13
|
+
)
|
14
|
+
}
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Authkeeper
|
4
|
+
module Providers
|
5
|
+
class Github
|
6
|
+
include AuthkeeperDeps[
|
7
|
+
auth_client: 'api.github.auth_client',
|
8
|
+
api_client: 'api.github.client'
|
9
|
+
]
|
10
|
+
|
11
|
+
def call(params: {})
|
12
|
+
access_token = fetch_access_token(params[:code])
|
13
|
+
return { errors: ['Invalid code'] } unless access_token
|
14
|
+
|
15
|
+
user = fetch_user_info(access_token)
|
16
|
+
email = fetch_user_emails(access_token, user)
|
17
|
+
|
18
|
+
{
|
19
|
+
result: {
|
20
|
+
uid: user['id'].to_s,
|
21
|
+
provider: 'github',
|
22
|
+
login: user['login'],
|
23
|
+
email: email
|
24
|
+
}
|
25
|
+
}
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def fetch_access_token(code)
|
31
|
+
auth_client.fetch_access_token(
|
32
|
+
client_id: omniauth_config[:client_id],
|
33
|
+
client_secret: omniauth_config[:client_secret],
|
34
|
+
code: code
|
35
|
+
)['access_token']
|
36
|
+
end
|
37
|
+
|
38
|
+
def fetch_user_info(access_token)
|
39
|
+
api_client.user(access_token: access_token)[:body]
|
40
|
+
end
|
41
|
+
|
42
|
+
def fetch_user_emails(access_token, user)
|
43
|
+
return user['email'] if user['email']
|
44
|
+
|
45
|
+
emails = api_client.user_emails(access_token: access_token)[:body]
|
46
|
+
emails.dig(0, 'email')
|
47
|
+
end
|
48
|
+
|
49
|
+
def omniauth_config
|
50
|
+
@omniauth_config ||= Authkeeper.configuration.omniauth_configs[:github]
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Authkeeper
|
4
|
+
module Providers
|
5
|
+
class Gitlab
|
6
|
+
include AuthkeeperDeps[
|
7
|
+
auth_client: 'api.gitlab.auth_client',
|
8
|
+
api_client: 'api.gitlab.client'
|
9
|
+
]
|
10
|
+
|
11
|
+
def call(params: {})
|
12
|
+
access_token = fetch_access_token(params[:code])
|
13
|
+
return { errors: ['Invalid code'] } unless access_token
|
14
|
+
|
15
|
+
user = fetch_user_info(access_token)
|
16
|
+
|
17
|
+
{
|
18
|
+
result: {
|
19
|
+
uid: user['id'].to_s,
|
20
|
+
provider: 'gitlab',
|
21
|
+
login: user['username'],
|
22
|
+
email: user['email']
|
23
|
+
}
|
24
|
+
}
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def fetch_access_token(code)
|
30
|
+
auth_client.fetch_access_token(
|
31
|
+
client_id: omniauth_config[:client_id],
|
32
|
+
client_secret: omniauth_config[:client_secret],
|
33
|
+
redirect_url: omniauth_config[:redirect_url],
|
34
|
+
code: code
|
35
|
+
)['access_token']
|
36
|
+
end
|
37
|
+
|
38
|
+
def fetch_user_info(access_token)
|
39
|
+
api_client.user(access_token: access_token)[:body]
|
40
|
+
end
|
41
|
+
|
42
|
+
def omniauth_config
|
43
|
+
@omniauth_config ||= Authkeeper.configuration.omniauth_configs[:gitlab]
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Authkeeper
|
4
|
+
module Providers
|
5
|
+
class Google
|
6
|
+
include AuthkeeperDeps[
|
7
|
+
auth_client: 'api.google.auth_client',
|
8
|
+
api_client: 'api.google.client'
|
9
|
+
]
|
10
|
+
|
11
|
+
def call(params: {})
|
12
|
+
access_token = fetch_access_token(params[:code])
|
13
|
+
return { errors: ['Invalid code'] } unless access_token
|
14
|
+
|
15
|
+
user = fetch_user_info(access_token)
|
16
|
+
|
17
|
+
{
|
18
|
+
result: {
|
19
|
+
uid: user['sub'].to_s,
|
20
|
+
provider: 'google',
|
21
|
+
email: user['email']
|
22
|
+
}
|
23
|
+
}
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def fetch_access_token(code)
|
29
|
+
auth_client.fetch_access_token(
|
30
|
+
client_id: omniauth_config[:client_id],
|
31
|
+
client_secret: omniauth_config[:client_secret],
|
32
|
+
redirect_uri: omniauth_config[:redirect_url],
|
33
|
+
code: code
|
34
|
+
)['access_token']
|
35
|
+
end
|
36
|
+
|
37
|
+
def fetch_user_info(access_token)
|
38
|
+
api_client.user(access_token: access_token)[:body]
|
39
|
+
end
|
40
|
+
|
41
|
+
def omniauth_config
|
42
|
+
@omniauth_config ||= Authkeeper.configuration.omniauth_configs[:google]
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'openssl'
|
4
|
+
require 'base64'
|
5
|
+
|
6
|
+
module Authkeeper
|
7
|
+
module Providers
|
8
|
+
class Telegram
|
9
|
+
REQUIRED_FIELDS = %i[id hash].freeze
|
10
|
+
HASH_FIELDS = %i[auth_date first_name id last_name photo_url username].freeze
|
11
|
+
SECONDS_IN_DAY = 86_400
|
12
|
+
|
13
|
+
def call(params: {})
|
14
|
+
return { errors: ['Required field is missing'] } unless required_fields_valid?(params)
|
15
|
+
return { errors: ['Signature mismatch'] } unless signature_valid?(params)
|
16
|
+
return { errors: ['Session expired'] } if session_expired?(params)
|
17
|
+
|
18
|
+
{
|
19
|
+
result: {
|
20
|
+
uid: params[:id].to_s,
|
21
|
+
provider: 'telegram',
|
22
|
+
login: params[:username]
|
23
|
+
}
|
24
|
+
}
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def required_fields_valid?(params)
|
30
|
+
REQUIRED_FIELDS.all? { |field| params.include?(field) }
|
31
|
+
end
|
32
|
+
|
33
|
+
def signature_valid?(params)
|
34
|
+
params[:hash] == calculate_signature(params)
|
35
|
+
end
|
36
|
+
|
37
|
+
def session_expired?(params)
|
38
|
+
Time.now.to_i - params[:auth_date].to_i > SECONDS_IN_DAY
|
39
|
+
end
|
40
|
+
|
41
|
+
def calculate_signature(params)
|
42
|
+
secret = OpenSSL::Digest::SHA256.digest(omniauth_config[:bot_secret])
|
43
|
+
signature = generate_comparison_string(params)
|
44
|
+
OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('SHA256'), secret, signature)
|
45
|
+
end
|
46
|
+
|
47
|
+
# rubocop: disable Style/FormatStringToken
|
48
|
+
def generate_comparison_string(params)
|
49
|
+
(params.keys & HASH_FIELDS).sort.map { |field| '%s=%s' % [field, params[field]] }.join("\n")
|
50
|
+
end
|
51
|
+
# rubocop: enable Style/FormatStringToken
|
52
|
+
|
53
|
+
def omniauth_config
|
54
|
+
@omniauth_config ||= Authkeeper.configuration.omniauth_configs[:telegram]
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Authkeeper
|
4
|
+
module Providers
|
5
|
+
class Yandex
|
6
|
+
include Deps[
|
7
|
+
auth_client: 'api.yandex.auth_client',
|
8
|
+
api_client: 'api.yandex.client'
|
9
|
+
]
|
10
|
+
|
11
|
+
def call(params: {})
|
12
|
+
auth_info = fetch_auth_info(params[:code])
|
13
|
+
return { errors: ['Invalid code'] } if auth_info.nil?
|
14
|
+
return { errors: ['Invalid code'] } unless auth_info['access_token']
|
15
|
+
|
16
|
+
user_info = fetch_user_info(auth_info['access_token'])
|
17
|
+
{
|
18
|
+
result: {
|
19
|
+
auth_info: auth_info.symbolize_keys,
|
20
|
+
user_info: {
|
21
|
+
uid: user_info['id'].to_s,
|
22
|
+
provider: 'yandex',
|
23
|
+
username: user_info['login'],
|
24
|
+
email: user_info['default_email'],
|
25
|
+
phone_number: user_info.dig('default_phone', 'number')
|
26
|
+
}
|
27
|
+
}
|
28
|
+
}
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def fetch_auth_info(code)
|
34
|
+
auth_client.fetch_access_token(
|
35
|
+
client_id: omniauth_config[:client_id],
|
36
|
+
client_secret: omniauth_config[:client_secret],
|
37
|
+
code: code
|
38
|
+
)
|
39
|
+
end
|
40
|
+
|
41
|
+
def fetch_user_info(access_token)
|
42
|
+
api_client.info(access_token: access_token)
|
43
|
+
end
|
44
|
+
|
45
|
+
def omniauth_config
|
46
|
+
@omniauth_config ||= Authkeeper.configuration.omniauth_configs[:yandex]
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
data/config/routes.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Authkeeper
|
4
|
+
class Configuration
|
5
|
+
InitializeError = Class.new(StandardError)
|
6
|
+
|
7
|
+
attr_accessor :user_model, :user_session_model, :access_token_name, :domain, :fallback_url_session_name, :omniauth_providers
|
8
|
+
attr_reader :omniauth_configs
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
@user_model = 'User'
|
12
|
+
@user_session_model = 'User::Session'
|
13
|
+
|
14
|
+
@access_token_name = nil
|
15
|
+
@domain = nil
|
16
|
+
@fallback_url_session_name = nil
|
17
|
+
|
18
|
+
@omniauth_providers = []
|
19
|
+
@omniauth_configs = {}
|
20
|
+
end
|
21
|
+
|
22
|
+
def validate
|
23
|
+
raise InitializeError, 'User model must be present' if user_model.nil?
|
24
|
+
raise InitializeError, 'User session model must be present' if user_session_model.nil?
|
25
|
+
raise InitializeError, 'Access token name must be present' if access_token_name.nil?
|
26
|
+
end
|
27
|
+
|
28
|
+
def omniauth(provider, **args)
|
29
|
+
@omniauth_configs[provider] = args
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'dry/auto_inject'
|
4
|
+
require 'dry/container'
|
5
|
+
|
6
|
+
module Authkeeper
|
7
|
+
class Container
|
8
|
+
extend Dry::Container::Mixin
|
9
|
+
|
10
|
+
DEFAULT_OPTIONS = { memoize: true }.freeze
|
11
|
+
|
12
|
+
class << self
|
13
|
+
def register(key)
|
14
|
+
super(key, DEFAULT_OPTIONS)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
register(:jwt_encoder) { Authkeeper::JwtEncoder.new }
|
19
|
+
|
20
|
+
register('api.github.auth_client') { Authkeeper::GithubAuthApi::Client.new }
|
21
|
+
register('api.github.client') { Authkeeper::GithubApi::Client.new }
|
22
|
+
register('api.gitlab.auth_client') { Authkeeper::GitlabAuthApi::Client.new }
|
23
|
+
register('api.gitlab.client') { Authkeeper::GitlabApi::Client.new }
|
24
|
+
register('api.google.auth_client') { Authkeeper::GoogleAuthApi::Client.new }
|
25
|
+
register('api.google.client') { Authkeeper::GoogleApi::Client.new }
|
26
|
+
register('api.yandex.auth_client') { Authkeeper::YandexAuthApi::Client.new }
|
27
|
+
register('api.yandex.client') { Authkeeper::YandexApi::Client.new }
|
28
|
+
|
29
|
+
register('services.providers.github') { Authkeeper::Providers::Github.new }
|
30
|
+
register('services.providers.gitlab') { Authkeeper::Providers::Gitlab.new }
|
31
|
+
register('services.providers.telegram') { Authkeeper::Providers::Telegram.new }
|
32
|
+
register('services.providers.google') { Authkeeper::Providers::Google.new }
|
33
|
+
register('services.providers.yandex') { Authkeeper::Providers::Yandex.new }
|
34
|
+
|
35
|
+
register('services.fetch_session') { Authkeeper::FetchSessionService.new }
|
36
|
+
register('services.generate_token') { Authkeeper::GenerateTokenService.new }
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
AuthkeeperDeps = Dry::AutoInject(Authkeeper::Container)
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Authkeeper
|
4
|
+
module Controllers
|
5
|
+
module Authentication
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
included do
|
9
|
+
before_action :set_current_user
|
10
|
+
|
11
|
+
helper_method :current_user
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def set_current_user
|
17
|
+
access_token = cookies_token.presence || bearer_token.presence || params_token
|
18
|
+
return unless access_token
|
19
|
+
|
20
|
+
auth_call = Authkeeper::Container['services.fetch_session'].call(token: access_token)
|
21
|
+
return if auth_call[:errors].present?
|
22
|
+
|
23
|
+
@current_user = auth_call[:result].user
|
24
|
+
end
|
25
|
+
|
26
|
+
def current_user = @current_user
|
27
|
+
|
28
|
+
def authenticate
|
29
|
+
return if current_user
|
30
|
+
|
31
|
+
if Authkeeper.configuration.fallback_url_session_name
|
32
|
+
session[Authkeeper.configuration.fallback_url_session_name] = request.fullpath
|
33
|
+
end
|
34
|
+
|
35
|
+
authentication_error
|
36
|
+
end
|
37
|
+
|
38
|
+
def authentication_error
|
39
|
+
redirect_to root_path, alert: t('controllers.authentication.permission')
|
40
|
+
end
|
41
|
+
|
42
|
+
def cookies_token = cookies[access_token_name]
|
43
|
+
def params_token = params[access_token_name]
|
44
|
+
|
45
|
+
def bearer_token
|
46
|
+
pattern = /^Bearer /
|
47
|
+
header = request.headers['Authorization']
|
48
|
+
header.gsub(pattern, '') if header&.match(pattern)
|
49
|
+
end
|
50
|
+
|
51
|
+
def access_token_name = Authkeeper.configuration.access_token_name
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
data/lib/authkeeper.rb
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'authkeeper/version'
|
4
|
+
require 'authkeeper/engine'
|
5
|
+
require 'authkeeper/configuration'
|
6
|
+
require 'authkeeper/container'
|
7
|
+
|
8
|
+
require 'authkeeper/controllers/authentication'
|
9
|
+
|
10
|
+
module Authkeeper
|
11
|
+
module_function
|
12
|
+
|
13
|
+
# Public: Configure authkeeper.
|
14
|
+
#
|
15
|
+
# Authkeeper.configure do |config|
|
16
|
+
# config.user_model = 'User'
|
17
|
+
# config.omniauth :github, client_id: 'id', client_secret: 'secret', redirect_url: 'redirect_url'
|
18
|
+
# config.omniauth :gitlab, client_id: 'id', client_secret: 'secret', redirect_url: 'redirect_url'
|
19
|
+
# config.omniauth :google, client_id: 'id', client_secret: 'secret', redirect_uri: 'redirect_uri'
|
20
|
+
# config.omniauth :telegram, client_id: 'id', client_secret: 'secret', bot_secret: 'bot_secret'
|
21
|
+
# config.omniauth :yandex, client_id: 'id', client_secret: 'secret', redirect_uri: 'redirect_uri'
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
def configure
|
25
|
+
yield configuration
|
26
|
+
|
27
|
+
configuration.validate
|
28
|
+
end
|
29
|
+
|
30
|
+
# Public: Returns Authkeeper::Configuration instance.
|
31
|
+
def configuration
|
32
|
+
return Authkeeper::Container.resolve(:configuration) if Authkeeper::Container.key?(:configuration)
|
33
|
+
|
34
|
+
Authkeeper::Container.register(:configuration) { Configuration.new }
|
35
|
+
Authkeeper::Container.resolve(:configuration)
|
36
|
+
end
|
37
|
+
end
|
metadata
ADDED
@@ -0,0 +1,102 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: authkeeper
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Bogdanov Anton
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2024-11-20 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rails
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '7.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '7.0'
|
27
|
+
description: Authentication engine for Ruby on Rails projects.
|
28
|
+
email:
|
29
|
+
- kortirso@gmail.com
|
30
|
+
executables: []
|
31
|
+
extensions: []
|
32
|
+
extra_rdoc_files: []
|
33
|
+
files:
|
34
|
+
- README.md
|
35
|
+
- Rakefile
|
36
|
+
- app/assets/config/authkeeper_manifest.js
|
37
|
+
- app/assets/stylesheets/authkeeper/application.css
|
38
|
+
- app/controllers/authkeeper/omniauth_callbacks_controller.rb
|
39
|
+
- app/helpers/authkeeper/application_helper.rb
|
40
|
+
- app/jobs/authkeeper/application_job.rb
|
41
|
+
- app/lib/authkeeper/github_api/client.rb
|
42
|
+
- app/lib/authkeeper/github_api/requests/user.rb
|
43
|
+
- app/lib/authkeeper/github_api/requests/user_emails.rb
|
44
|
+
- app/lib/authkeeper/github_auth_api/client.rb
|
45
|
+
- app/lib/authkeeper/github_auth_api/requests/fetch_access_token.rb
|
46
|
+
- app/lib/authkeeper/gitlab_api/client.rb
|
47
|
+
- app/lib/authkeeper/gitlab_api/requests/user.rb
|
48
|
+
- app/lib/authkeeper/gitlab_auth_api/client.rb
|
49
|
+
- app/lib/authkeeper/gitlab_auth_api/requests/fetch_access_token.rb
|
50
|
+
- app/lib/authkeeper/google_api/client.rb
|
51
|
+
- app/lib/authkeeper/google_api/requests/user.rb
|
52
|
+
- app/lib/authkeeper/google_auth_api/client.rb
|
53
|
+
- app/lib/authkeeper/google_auth_api/requests/fetch_access_token.rb
|
54
|
+
- app/lib/authkeeper/jwt_encoder.rb
|
55
|
+
- app/lib/authkeeper/yandex_api/client.rb
|
56
|
+
- app/lib/authkeeper/yandex_api/requests/info.rb
|
57
|
+
- app/lib/authkeeper/yandex_auth_api/client.rb
|
58
|
+
- app/lib/authkeeper/yandex_auth_api/requests/fetch_access_token.rb
|
59
|
+
- app/mailers/authkeeper/application_mailer.rb
|
60
|
+
- app/models/authkeeper/application_record.rb
|
61
|
+
- app/services/authkeeper/fetch_session_service.rb
|
62
|
+
- app/services/authkeeper/generate_token_service.rb
|
63
|
+
- app/services/authkeeper/providers/github.rb
|
64
|
+
- app/services/authkeeper/providers/gitlab.rb
|
65
|
+
- app/services/authkeeper/providers/google.rb
|
66
|
+
- app/services/authkeeper/providers/telegram.rb
|
67
|
+
- app/services/authkeeper/providers/yandex.rb
|
68
|
+
- app/views/layouts/authkeeper/application.html.erb
|
69
|
+
- config/routes.rb
|
70
|
+
- lib/authkeeper.rb
|
71
|
+
- lib/authkeeper/configuration.rb
|
72
|
+
- lib/authkeeper/container.rb
|
73
|
+
- lib/authkeeper/controllers/authentication.rb
|
74
|
+
- lib/authkeeper/engine.rb
|
75
|
+
- lib/authkeeper/version.rb
|
76
|
+
homepage: https://github.com/kortirso/authkeeper
|
77
|
+
licenses:
|
78
|
+
- MIT
|
79
|
+
metadata:
|
80
|
+
homepage_uri: https://github.com/kortirso/authkeeper
|
81
|
+
source_code_uri: https://github.com/kortirso/authkeeper
|
82
|
+
changelog_uri: https://github.com/kortirso/authkeeper/blob/master/CHANGELOG.md
|
83
|
+
post_install_message:
|
84
|
+
rdoc_options: []
|
85
|
+
require_paths:
|
86
|
+
- lib
|
87
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
88
|
+
requirements:
|
89
|
+
- - ">="
|
90
|
+
- !ruby/object:Gem::Version
|
91
|
+
version: '3.2'
|
92
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
requirements: []
|
98
|
+
rubygems_version: 3.4.1
|
99
|
+
signing_key:
|
100
|
+
specification_version: 4
|
101
|
+
summary: Authentication engine.
|
102
|
+
test_files: []
|