authkeeper 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|