holistic_auth 0.9.8

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