holistic_auth 0.9.8

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.
@@ -0,0 +1,35 @@
1
+ module HolisticAuth
2
+ class EndPointListener
3
+ attr_reader :provider, :auth_code, :options, :errors
4
+ def initialize(hash = {})
5
+ @provider = hash.delete :provider
6
+ @auth_code = hash.delete :auth_code
7
+ @options = hash
8
+ @errors = []
9
+ end
10
+
11
+ def valid?
12
+ validator_presence? &&
13
+ validator_valid_provider?
14
+ end
15
+
16
+ private
17
+
18
+ def validator_presence?
19
+ provider_present = @provider.present?
20
+ auth_code_present = @auth_code.present?
21
+
22
+ return true if provider_present && auth_code_present
23
+ errors << 'A required param is missing'
24
+ errors << '"provider" field missing' unless provider_present
25
+ errors << '"auth_code" field missing' unless auth_code_present
26
+ false
27
+ end
28
+
29
+ def validator_valid_provider?
30
+ return true if @provider.is_a? HolisticAuth::Providers::GenericProvider
31
+ errors << "Provider '#{@provider}' is invalid"
32
+ false
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,4 @@
1
+ module HolisticAuth
2
+ class EmailNotVerifiedError < ArgumentError
3
+ end
4
+ end
@@ -0,0 +1,56 @@
1
+ module HolisticAuth
2
+ module OrmHandlers
3
+ class ActiveRecord
4
+ # TODO: Railtie this properly rather than hacks
5
+ require 'active_record'
6
+
7
+ attr_reader :info, :account, :provider_name
8
+
9
+ def initialize(info, provider_name)
10
+ @info = info
11
+ @provider_name = provider_name
12
+ end
13
+
14
+ def discover_user!
15
+ discover_account!.user
16
+ end
17
+
18
+ def discover_account!
19
+ @account.present? ? @account : (@account = find_or_create_account)
20
+ end
21
+
22
+ def store_provider_credentials!(access_token)
23
+ raise 'Account not discovered yet!' unless @account.present?
24
+
25
+ @account.replace_credential!(
26
+ access_token: access_token.token,
27
+ refresh_token: access_token.refresh_token,
28
+ expires_at: (Time.now.utc + access_token.expires_in),
29
+ expires: access_token.expires_in.present? ? true : false,
30
+ )
31
+ end
32
+
33
+ private
34
+
35
+ def find_or_create_account
36
+ Account.find_by(email: info[:email], provider: @provider_name) || create_account!
37
+ end
38
+
39
+ def create_account!
40
+ user = User.find_by(primary_email: info[:email]) || create_user!
41
+ user.create_account!(
42
+ email: info[:email],
43
+ provider: @provider_name,
44
+ provider_uid: info[:uid],
45
+ )
46
+ end
47
+
48
+ def create_user!
49
+ User.create!(
50
+ primary_email: info[:email], display_name: info[:display_name],
51
+ name: info[:name], picture_url: info[:picture_url]
52
+ )
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,100 @@
1
+ module HolisticAuth
2
+ module Providers
3
+ require 'oauth2'
4
+
5
+ class GenericProvider
6
+ attr_accessor :client_id,
7
+ :client_secret,
8
+ :site,
9
+ :tenant_id,
10
+ :token_url,
11
+ :api_key,
12
+ :user_info_url
13
+
14
+ attr_reader :oauth2_client
15
+
16
+ def initialize(options = {})
17
+ @client_id = options.delete :client_id
18
+ @client_secret = options.delete :client_secret
19
+ @site = options.delete(:site) || settings[:site]
20
+ @token_url = options.delete(:token_url) || settings[:token_url]
21
+ @api_key = options.delete :api_key
22
+ @user_info_url = options.delete(:user_info_url) || settings[:user_info_url]
23
+ @additional_parameters = options.delete(:additional_parameters) || settings[:additional_parameters] || {}
24
+ end
25
+
26
+ def add_secrets(options = {})
27
+ @client_id = options.delete :client_id if options[:client_id]
28
+ @client_secret = options.delete :client_secret if options[:client_secret]
29
+ @api_key = options.delete :api_key if options[:api_key]
30
+ @tenant_id = options.delete :tenant_id if options[:tenant_id]
31
+ @additional_parameters.merge!(options.delete(:additional_parameters)) if options[:additional_parameters]
32
+ end
33
+
34
+ def settings
35
+ {}
36
+ end
37
+
38
+ def secrets
39
+ sec = {}
40
+ sec[:client_id] = @client_id if @client_id
41
+ sec[:client_secret] = @client_secret if @client_secret
42
+ sec[:api_key] = @api_key if @api_key
43
+
44
+ sec
45
+ end
46
+
47
+ def exchange(auth_code, redirect_uri)
48
+ errors = []
49
+ errors << 'auth_code missing' if auth_code.blank?
50
+ errors << 'redirect_uri missing' if redirect_uri.blank?
51
+ errors << 'Client ID not set' if client_id.blank?
52
+ errors << 'Client Secret not set' if client_secret.blank?
53
+
54
+ raise "Cannot exchange auth code:\n#{errors}" if errors.present?
55
+
56
+ @oauth2_client = OAuth2::Client.new(client_id, client_secret, to_hash)
57
+ @oauth2_client.auth_code.get_token(auth_code, redirect_uri: redirect_uri)
58
+ end
59
+
60
+ def retrieve_user_info(*_params)
61
+ raise 'Not implemented'
62
+ end
63
+
64
+ def name(*_params)
65
+ raise 'Generic provider doesn\'t have a name'
66
+ end
67
+
68
+ def full_site_url
69
+ raise "site not specified for class #{self}" unless site.present?
70
+ site
71
+ end
72
+
73
+ def site_token_url
74
+ full_site_url + token_url
75
+ end
76
+
77
+ def to_hash
78
+ {
79
+ client_id: @client_id,
80
+ client_secret: @client_secret,
81
+ site: full_site_url,
82
+ token_url: @token_url,
83
+ api_key: @api_key,
84
+ user_info_url: @user_info_url,
85
+ additional_parameters: @additional_parameters,
86
+ }
87
+ end
88
+
89
+ def present?
90
+ !empty?
91
+ end
92
+
93
+ def empty?
94
+ site.nil? || token_url.nil?
95
+ end
96
+
97
+ alias inspect to_hash
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,82 @@
1
+ module HolisticAuth
2
+ module Providers
3
+ class Google < GenericProvider
4
+ SETTINGS = {
5
+ site: 'https://accounts.google.com',
6
+ token_url: '/o/oauth2/token',
7
+ user_info_url: 'https://www.googleapis.com/plus/v1/people/me/openIdConnect',
8
+ }.freeze
9
+
10
+ def settings
11
+ SETTINGS
12
+ end
13
+
14
+ def name
15
+ :google
16
+ end
17
+
18
+ def retrieve_user_info(access_token)
19
+ client = GoogleClient::Builder.new('plus', 'v1', 1)
20
+ result = client.execute access_token,
21
+ api_method: client.service.people.get,
22
+ parameters: {
23
+ userId: 'me',
24
+ }
25
+
26
+ process_user_info JSON.parse(result.body)
27
+ end
28
+
29
+ private
30
+
31
+ def process_user_info(hash)
32
+ {
33
+ email_verified: hash['emails'].first['type'].eql?('account'),
34
+ email: hash['emails'].first['value'],
35
+ display_name: hash['displayName'],
36
+ name: hash['name'],
37
+ picture_url: hash['image']['url'],
38
+ uid: hash['id'],
39
+ language: hash['language'],
40
+ }.with_indifferent_access
41
+
42
+ # {
43
+ # "kind" => "plus#person",
44
+ # "etag" => "\"xyz-something/abc\"",
45
+ # "gender" => "female",
46
+ # "emails" => [
47
+ # {
48
+ # "value" => "info@foogi.me",
49
+ # "type" => "account"
50
+ # }
51
+ # ],
52
+ # "objectType" => "person",
53
+ # "id" => "12345",
54
+ # "displayName" => "Leading Foogster",
55
+ # "name" => {
56
+ # "familyName" => "Foogster",
57
+ # "givenName" => "Leading"
58
+ # },
59
+ # "url" => "https://plus.google.com/+FoogiMe",
60
+ # "image" => {
61
+ # "url" => "https://someurl/photo.jpg?sz=50",
62
+ # "isDefault" => false
63
+ # },
64
+ # "placesLived" => [
65
+ # {
66
+ # "value" => "Sydney, Australia",
67
+ # "primary" => true
68
+ # },
69
+ # {
70
+ # "value" => "San Francisco, USA"
71
+ # }
72
+ # ],
73
+ # "isPlusUser" => true,
74
+ # "language" => "en_GB",
75
+ # "circledByCount" => 11225,
76
+ # "verified" => false,
77
+ # "domain" => "foogi.me"
78
+ # }
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,93 @@
1
+ module HolisticAuth
2
+ module Providers
3
+ class MsGraph < GenericProvider
4
+ GRAPH_RESOURCE = 'https://graph.microsoft.com'.freeze
5
+ DEFAULT_CONTENT_TYPE = 'application/json;odata.metadata=minimal;odata.streaming=true'.freeze
6
+ API_VERSION = 'beta'.freeze
7
+
8
+ SETTINGS = {
9
+ site: 'https://login.microsoftonline.com',
10
+ token_url: 'oauth2/token',
11
+ user_info_url: URI("#{GRAPH_RESOURCE}/#{API_VERSION}/me"),
12
+ additional_parameters: {
13
+ resource: GRAPH_RESOURCE,
14
+ },
15
+ }.freeze
16
+
17
+ def settings
18
+ self.class::SETTINGS
19
+ end
20
+
21
+ def name
22
+ :ms_graph
23
+ end
24
+
25
+ def full_site_url
26
+ tenant_id.present? ? (site + '/' + tenant_id + '/') : (site + '/common/')
27
+ end
28
+
29
+ def retrieve_user_info(access_token)
30
+ result = query! :get, access_token.token, settings[:user_info_url]
31
+ process_info JSON.parse(result.body)
32
+ end
33
+
34
+ def process_info(hash)
35
+ sanity_check! hash
36
+
37
+ {
38
+ email_verified: hash['mail'].present?,
39
+ email: hash['mail'],
40
+ display_name: hash['displayName'],
41
+ name: {
42
+ givenName: hash['givenName'],
43
+ familyName: hash['familyName'],
44
+ },
45
+ picture_url: '',
46
+ uid: hash['id'],
47
+ language: hash['preferredLanguage'],
48
+ }.with_indifferent_access
49
+ end
50
+
51
+ # def events
52
+ # query_params = {
53
+ # startdatetime: DateTime.now.utc,
54
+ # enddatetime: DateTime.now.utc + 2.months,
55
+ # '$orderby' => 'start/dateTime',
56
+ # }
57
+ # end
58
+
59
+ # Need error handling for when the token has expired.
60
+ def query!(method, access_token, uri, body = nil)
61
+ http = Net::HTTP.new(uri.host, uri.port)
62
+ http.use_ssl = true
63
+
64
+ headers = {
65
+ 'Authorization' => "Bearer #{access_token}",
66
+ 'Content-Type' => DEFAULT_CONTENT_TYPE,
67
+ }
68
+
69
+ full_endpoint = uri.query.present? ? "#{uri.path}?#{uri.query}" : uri.path
70
+
71
+ response =
72
+ case method
73
+ when :get
74
+ http.get(full_endpoint, headers)
75
+ when :post
76
+ http.post(full_endpoint, body, headers)
77
+ else
78
+ raise "method #{method} not implemented"
79
+ end
80
+
81
+ response
82
+ end
83
+
84
+ def sanity_check!(hash)
85
+ raise "Can't process empty user info" unless hash.is_a? Hash
86
+
87
+ if hash.key?('error')
88
+ raise "Could not process user info: \n #{hash['error']['code']}: #{hash['error']['message']}"
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,38 @@
1
+ module HolisticAuth
2
+ module Providers
3
+ class Outlook < MsGraph
4
+ RESOURCE = 'https://outlook.office.com'.freeze
5
+ API_VERSION = 'v2.0'.freeze
6
+
7
+ SETTINGS = {
8
+ site: 'https://login.microsoftonline.com',
9
+ token_url: 'oauth2/v2.0/token',
10
+ user_info_url: URI("#{RESOURCE}/api/#{API_VERSION}/Me"),
11
+ additional_parameters: {
12
+ resource: RESOURCE,
13
+ },
14
+ }.freeze
15
+
16
+ def name
17
+ :outlook
18
+ end
19
+
20
+ def process_info(hash)
21
+ sanity_check!(hash)
22
+
23
+ {
24
+ email_verified: hash['EmailAddress'].present?,
25
+ email: hash['EmailAddress'],
26
+ display_name: hash['DisplayName'],
27
+ name: {
28
+ givenName: hash['givenName'],
29
+ familyName: hash['familyName'],
30
+ },
31
+ picture_url: '',
32
+ uid: hash['Id'],
33
+ language: hash['preferredLanguage'],
34
+ }.with_indifferent_access
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,60 @@
1
+ module HolisticAuth
2
+ module Providers
3
+ class Stub < GenericProvider
4
+ SETTINGS = {
5
+ client_id: 'stub_cl_id',
6
+ client_secret: 'stub_cl_sec',
7
+ site: 'https://example.org',
8
+ token_url: '/extoken',
9
+ api_key: 'api_key',
10
+ user_info_url: 'http://example.org/info',
11
+ }.freeze
12
+
13
+ STUB_SAMPLE_TOKEN = {
14
+ token: 'ya29.token',
15
+ refresh_token: '1/refresh',
16
+ expires_in: 3600,
17
+ }.freeze
18
+
19
+ def initialize(options = {})
20
+ super(options)
21
+
22
+ @client_id ||= settings[:client_id]
23
+ @client_secret ||= settings[:client_secret]
24
+ @api_key ||= settings[:api_key]
25
+ end
26
+
27
+ def settings
28
+ SETTINGS
29
+ end
30
+
31
+ def name
32
+ :stub
33
+ end
34
+
35
+ def exchange(_, __)
36
+ @client = OAuth2::Client.new(
37
+ client_id,
38
+ client_secret,
39
+ )
40
+
41
+ OAuth2::AccessToken.new(
42
+ @client,
43
+ STUB_SAMPLE_TOKEN[:token],
44
+ refresh_token: STUB_SAMPLE_TOKEN[:refresh_token],
45
+ expires_in: STUB_SAMPLE_TOKEN[:expires_in],
46
+ )
47
+ end
48
+
49
+ def retrieve_user_info(_access_token)
50
+ {
51
+ email_verified: true,
52
+ email: 'a@b.c',
53
+ given_name: 'first',
54
+ family_name: 'last',
55
+ profile: 'xyz',
56
+ }.with_indifferent_access
57
+ end
58
+ end
59
+ end
60
+ end