authkeeper 0.1.0

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