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.
Files changed (44) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +28 -0
  3. data/Rakefile +10 -0
  4. data/app/assets/config/authkeeper_manifest.js +1 -0
  5. data/app/assets/stylesheets/authkeeper/application.css +15 -0
  6. data/app/controllers/authkeeper/omniauth_callbacks_controller.rb +68 -0
  7. data/app/helpers/authkeeper/application_helper.rb +38 -0
  8. data/app/jobs/authkeeper/application_job.rb +6 -0
  9. data/app/lib/authkeeper/github_api/client.rb +24 -0
  10. data/app/lib/authkeeper/github_api/requests/user.rb +16 -0
  11. data/app/lib/authkeeper/github_api/requests/user_emails.rb +16 -0
  12. data/app/lib/authkeeper/github_auth_api/client.rb +13 -0
  13. data/app/lib/authkeeper/github_auth_api/requests/fetch_access_token.rb +17 -0
  14. data/app/lib/authkeeper/gitlab_api/client.rb +21 -0
  15. data/app/lib/authkeeper/gitlab_api/requests/user.rb +16 -0
  16. data/app/lib/authkeeper/gitlab_auth_api/client.rb +13 -0
  17. data/app/lib/authkeeper/gitlab_auth_api/requests/fetch_access_token.rb +23 -0
  18. data/app/lib/authkeeper/google_api/client.rb +13 -0
  19. data/app/lib/authkeeper/google_api/requests/user.rb +13 -0
  20. data/app/lib/authkeeper/google_auth_api/client.rb +13 -0
  21. data/app/lib/authkeeper/google_auth_api/requests/fetch_access_token.rb +23 -0
  22. data/app/lib/authkeeper/jwt_encoder.rb +25 -0
  23. data/app/lib/authkeeper/yandex_api/client.rb +13 -0
  24. data/app/lib/authkeeper/yandex_api/requests/info.rb +16 -0
  25. data/app/lib/authkeeper/yandex_auth_api/client.rb +13 -0
  26. data/app/lib/authkeeper/yandex_auth_api/requests/fetch_access_token.rb +26 -0
  27. data/app/mailers/authkeeper/application_mailer.rb +8 -0
  28. data/app/models/authkeeper/application_record.rb +7 -0
  29. data/app/services/authkeeper/fetch_session_service.rb +29 -0
  30. data/app/services/authkeeper/generate_token_service.rb +17 -0
  31. data/app/services/authkeeper/providers/github.rb +54 -0
  32. data/app/services/authkeeper/providers/gitlab.rb +47 -0
  33. data/app/services/authkeeper/providers/google.rb +46 -0
  34. data/app/services/authkeeper/providers/telegram.rb +58 -0
  35. data/app/services/authkeeper/providers/yandex.rb +50 -0
  36. data/app/views/layouts/authkeeper/application.html.erb +12 -0
  37. data/config/routes.rb +6 -0
  38. data/lib/authkeeper/configuration.rb +32 -0
  39. data/lib/authkeeper/container.rb +40 -0
  40. data/lib/authkeeper/controllers/authentication.rb +54 -0
  41. data/lib/authkeeper/engine.rb +7 -0
  42. data/lib/authkeeper/version.rb +5 -0
  43. data/lib/authkeeper.rb +37 -0
  44. 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,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/setup'
4
+
5
+ APP_RAKEFILE = File.expand_path('test/dummy/Rakefile', __dir__)
6
+ load 'rails/tasks/engine.rake'
7
+
8
+ load 'rails/tasks/statistics.rake'
9
+
10
+ require 'bundler/gem_tasks'
@@ -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,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Authkeeper
4
+ class ApplicationJob < ActiveJob::Base
5
+ end
6
+ 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,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Authkeeper
4
+ module GithubApi
5
+ module Requests
6
+ module User
7
+ def user(access_token:)
8
+ get(
9
+ path: 'user',
10
+ headers: headers(access_token)
11
+ )
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Authkeeper
4
+ module GithubApi
5
+ module Requests
6
+ module UserEmails
7
+ def user_emails(access_token:)
8
+ get(
9
+ path: 'user/emails',
10
+ headers: headers(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 GithubAuthApi
5
+ class Client < HttpService::Client
6
+ include Requests::FetchAccessToken
7
+
8
+ BASE_URL = 'https://github.com'
9
+
10
+ option :url, default: proc { BASE_URL }
11
+ end
12
+ end
13
+ 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,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Authkeeper
4
+ module GitlabApi
5
+ module Requests
6
+ module User
7
+ def user(access_token:)
8
+ get(
9
+ path: 'api/v4/user',
10
+ headers: headers(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 GitlabAuthApi
5
+ class Client < HttpService::Client
6
+ include Requests::FetchAccessToken
7
+
8
+ BASE_URL = 'https://gitlab.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 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 GoogleApi
5
+ class Client < HttpService::Client
6
+ include Requests::User
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,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Authkeeper
4
+ module GoogleApi
5
+ module Requests
6
+ module User
7
+ def user(access_token:)
8
+ get(path: "oauth2/v3/userinfo?access_token=#{access_token}")
9
+ end
10
+ end
11
+ end
12
+ end
13
+ 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,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Authkeeper
4
+ module YandexApi
5
+ class Client < HttpService::Client
6
+ include Requests::Info
7
+
8
+ BASE_URL = 'https://login.yandex.ru/'
9
+
10
+ option :url, default: proc { BASE_URL }
11
+ end
12
+ end
13
+ 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,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Authkeeper
4
+ class ApplicationMailer < ActionMailer::Base
5
+ default from: 'from@example.com'
6
+ layout 'mailer'
7
+ end
8
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Authkeeper
4
+ class ApplicationRecord < ActiveRecord::Base
5
+ self.abstract_class = true
6
+ end
7
+ 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
@@ -0,0 +1,12 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Authkeeper</title>
5
+ <%= csrf_meta_tags %>
6
+ <%= csp_meta_tag %>
7
+ <%= stylesheet_link_tag "authkeeper/application", media: "all" %>
8
+ </head>
9
+ <body>
10
+ <%= yield %>
11
+ </body>
12
+ </html>
data/config/routes.rb ADDED
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubocop: disable Lint/EmptyBlock
4
+ Authkeeper::Engine.routes.draw do
5
+ end
6
+ # rubocop: enable Lint/EmptyBlock
@@ -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
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Authkeeper
4
+ class Engine < ::Rails::Engine
5
+ isolate_namespace Authkeeper
6
+ end
7
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Authkeeper
4
+ VERSION = '0.1.0'
5
+ 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: []