omniauth-azure_active_directory_b2c 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 0c0f7094e772b9c4aeeac8eba5ee0e5f8664c863
4
+ data.tar.gz: d48a92be522a55bb07ebbaca2d279a7fb84e825c
5
+ SHA512:
6
+ metadata.gz: 8dd3cdf6e26aec04a21bd50d7030ba583cc1d76a31b5e48dc7f726d1aa792bfacc2ef0dff1535b79a3e29aa52b7d7f8f4cafb28c09f543b308601042807bd567
7
+ data.tar.gz: 996b5025889be29a5dd93f4964f0aed570be8fe26ca2aef3506b4dcd34b53c82aa66e8c18906117242297f479f69c193bf80a453546ed2c4b87fff6d85c5f6ea
@@ -0,0 +1 @@
1
+ require_relative 'omniauth/strategies/azure_active_directory_b2c.rb'
@@ -0,0 +1,163 @@
1
+ require 'omniauth'
2
+ require 'proc_evaluate'
3
+
4
+ require_relative 'azure_active_directory_b2c/authentication_request.rb'
5
+ require_relative 'azure_active_directory_b2c/authentication_response.rb'
6
+ require_relative 'azure_active_directory_b2c/client.rb'
7
+ require_relative 'azure_active_directory_b2c/jwt_validator.rb'
8
+ require_relative 'azure_active_directory_b2c/policy_options.rb'
9
+ require_relative 'azure_active_directory_b2c/policy.rb'
10
+ require_relative 'azure_active_directory_b2c/version.rb'
11
+
12
+ module OmniAuth
13
+ module Strategies
14
+ class AzureActiveDirectoryB2C
15
+
16
+ include OmniAuth::Strategy
17
+
18
+ using ProcEvaluate # adds the `evaluate` refinement method to Object and Proc instances
19
+
20
+ #########################################
21
+ # Error definitions
22
+ #########################################
23
+
24
+ GenericError = Class.new(StandardError)
25
+
26
+ # Errors raised du to missing options or settings
27
+ MissingOptionError = Class.new(GenericError)
28
+
29
+ # Errors raised during the callback stage
30
+ CallbackError = Class.new(GenericError) do
31
+ attr_reader :failure_message_key
32
+ def self.failure_message_key(key)
33
+ define_method(:failure_message_key) { key }
34
+ end
35
+ end
36
+ InvalidCredentialsError = Class.new(CallbackError) { failure_message_key :invalid_credentials }
37
+ UnauthorizedError = Class.new(CallbackError) { failure_message_key :unauthorized }
38
+ MissingCodeError = Class.new(CallbackError) { failure_message_key :missing_code }
39
+ IdTokenValidationError = Class.new(CallbackError) { failure_message_key :id_token_validation_failed }
40
+
41
+ #########################################
42
+ # Strategy options
43
+ #########################################
44
+
45
+ option :name, 'azure_active_directory_b2c'
46
+ option :redirect_uri # the url to return to in the callback phase
47
+ option :policy_options
48
+
49
+ #########################################
50
+ # Strategy - setup
51
+ #########################################
52
+
53
+ def policy_options
54
+ @policy_options ||= options.policy_options || (raise MissingOptionError, '`policy_options` not defined')
55
+ end
56
+
57
+ def policy
58
+ @policy = Policy.new(**policy_options.symbolize_keys)
59
+ end
60
+
61
+ def redirect_uri
62
+ @redirect_uri ||= options.redirect_uri.evaluate(self) || (raise MissingOptionError, '`redirect_uri` not defined')
63
+ end
64
+
65
+ def setup_phase
66
+ end
67
+
68
+ #########################################
69
+ # Strategy - request
70
+ #########################################
71
+
72
+ def authentication_request
73
+ @authentication_request ||= AuthenticationRequest.new(policy, redirect_uri: redirect_uri)
74
+ end
75
+
76
+ def authorization_uri
77
+ authentication_request.authorization_uri
78
+ end
79
+
80
+ def set_session_variables!
81
+ # set the session details to check against in the callback_phase
82
+ session['omniauth.nonce'] = authentication_request.nonce
83
+ session['omniauth.state'] = authentication_request.state
84
+ end
85
+
86
+ def request_phase
87
+ # this phase needs to redirect to B2C with the correct params and record the state and nonce in the session to check against in the callback_phase
88
+ set_session_variables!
89
+ redirect authentication_request.authorization_uri
90
+ end
91
+
92
+ #########################################
93
+ # Strategy - callback
94
+ #########################################
95
+
96
+ def authentication_response
97
+ @authentication_response ||= AuthenticationResponse.new(policy, request.params['code'])
98
+ end
99
+
100
+ def callback_phase
101
+ validate_callback_response!
102
+ validate_id_token!
103
+ super # required to complete the callback phase
104
+
105
+ rescue UnauthorizedError => e
106
+ return Rack::Response.new(['401 Unauthorized'], 401).finish
107
+ rescue CallbackError => e
108
+ fail!(e.failure_message_key, e)
109
+ rescue ::Timeout::Error, ::Errno::ETIMEDOUT => e
110
+ fail!(:timeout, e)
111
+ rescue ::SocketError => e
112
+ fail!(:failed_to_connect, e)
113
+ end
114
+
115
+ def validate_callback_response!
116
+ state, code, error, error_reason, error_description = request.params.values_at('state', 'code', 'error', 'error_reason', 'error_description')
117
+
118
+ if error || error_reason || error_description
119
+ raise InvalidCredentialsError, [error, error_reason, error_description].compact.join('. ')
120
+ elsif state.to_s.empty? || state != session.delete('omniauth.state')
121
+ raise UnauthorizedError
122
+ elsif !code
123
+ raise MissingCodeError, 'Code was not returned from OpenID Connect Provider'
124
+ end
125
+ end
126
+
127
+ def validate_id_token!
128
+ results = authentication_response.validate_id_token
129
+ if results.has_errors?
130
+ raise IdTokenValidationError, results.full_messages.join('. ')
131
+ end
132
+ end
133
+
134
+ #########################################
135
+ # Auth Hash Schema
136
+ #########################################
137
+
138
+ def user_info
139
+ authentication_response.user_info
140
+ end
141
+
142
+ def subject_id
143
+ authentication_response.subject_id
144
+ end
145
+
146
+ def extra_info
147
+ authentication_response.extra_info
148
+ end
149
+
150
+ def credentials
151
+ authentication_response.credentials
152
+ end
153
+
154
+ # return the details required by OmniAuth
155
+ info { user_info }
156
+ uid { subject_id }
157
+ extra { extra_info }
158
+ credentials { credentials }
159
+
160
+ end
161
+ end
162
+ end
163
+ OmniAuth.config.add_camelization('azure_active_directory_b2c', 'AzureActiveDirectoryB2C')
@@ -0,0 +1,48 @@
1
+ module OmniAuth
2
+ module Strategies
3
+ class AzureActiveDirectoryB2C
4
+ class AuthenticationRequest
5
+
6
+ class ResponseType
7
+ # TODO: provide constants for each option
8
+ CODE = 'code'
9
+ end
10
+
11
+ attr_reader :policy, :client
12
+
13
+ def initialize(policy, redirect_uri:, **override_options)
14
+ @policy = policy
15
+ @client = policy.initialize_client({ redirect_uri: redirect_uri, **override_options })
16
+ end
17
+
18
+ def authorization_uri(**override_options)
19
+ options = default_authorization_uri_options.merge(override_options)
20
+ options = options.select {|k, v| v }
21
+ client.authorization_uri(options)
22
+ end
23
+
24
+ def state
25
+ @state ||= SecureRandom.hex(16)
26
+ end
27
+
28
+ def nonce
29
+ @nonce ||= SecureRandom.hex(16)
30
+ end
31
+
32
+ def response_type
33
+ ResponseType::CODE
34
+ end
35
+
36
+ def default_authorization_uri_options
37
+ {
38
+ response_type: response_type,
39
+ scope: policy.scope,
40
+ state: state,
41
+ nonce: nonce,
42
+ }
43
+ end
44
+
45
+ end # AuthenticationRequest
46
+ end # AzureActiveDirectoryB2C
47
+ end # Strategies
48
+ end # OmniAuth
@@ -0,0 +1,122 @@
1
+ module OmniAuth
2
+ module Strategies
3
+ class AzureActiveDirectoryB2C
4
+ class AuthenticationResponse
5
+
6
+ class AuthenticationMethod
7
+ BASIC = 'basic'
8
+ BODY = 'body'
9
+ POST = 'post'
10
+ end
11
+
12
+ attr_reader :policy, :client, :code
13
+
14
+ def initialize(policy, code, **override_options)
15
+ @policy = policy
16
+ @client = policy.initialize_client({ redirect_uri: nil, **override_options })
17
+ @client.authorization_code = code
18
+ @code = code
19
+ end
20
+
21
+ def access_token
22
+ @access_token ||= get_access_token!
23
+ end
24
+
25
+ def id_token
26
+ @id_token ||= get_id_token!
27
+ end
28
+
29
+ def refresh_token
30
+ access_token.refresh_token
31
+ end
32
+
33
+ def expires_in
34
+ access_token.expires_in
35
+ end
36
+
37
+ def subject_id
38
+ id_token.sub
39
+ end
40
+
41
+ def user_info
42
+ {
43
+ name: id_token.raw_attributes['name'],
44
+ email: ([*id_token.raw_attributes['emails']].first),
45
+ nickname: id_token.raw_attributes['preferred_username'],
46
+ first_name: id_token.raw_attributes['given_name'],
47
+ last_name: id_token.raw_attributes['family_name'],
48
+ gender: id_token.raw_attributes['gender'],
49
+ image: id_token.raw_attributes['picture'],
50
+ phone: id_token.raw_attributes['phone_number'],
51
+ urls: { website: id_token.raw_attributes['website'] }
52
+ }
53
+ end
54
+
55
+ def extra_info
56
+ { raw_info: id_token.raw_attributes }
57
+ end
58
+
59
+ def scope
60
+ policy.scope
61
+ end
62
+
63
+ def authentication_method
64
+ AuthenticationMethod::BODY
65
+ end
66
+
67
+ def credentials
68
+ {
69
+ id_token: id_token,
70
+ token: access_token.access_token,
71
+ refresh_token: refresh_token,
72
+ expires_in: expires_in,
73
+ scope: scope,
74
+ }
75
+ end
76
+
77
+ def default_access_token_options
78
+ {
79
+ scope: scope,
80
+ client_auth_method: authentication_method,
81
+ }
82
+ end
83
+
84
+ def get_access_token!
85
+ client.access_token!(default_access_token_options)
86
+ end
87
+
88
+ def get_id_token!
89
+ # TODO: if the id_token is not passed back, we could get the id token from the userinfo endpoint (or fail if no endpoint is defined?)
90
+ encrypted_id_token = access_token.id_token
91
+ decoded_id_token = decode_id_token!(encrypted_id_token)
92
+ end
93
+
94
+ def decode_id_token!(id_token)
95
+ ::OpenIDConnect::ResponseObject::IdToken.decode(id_token, public_key)
96
+ end
97
+
98
+ def public_key
99
+ if policy.jwk_signing_algorithm == :RS256 && policy.jwk_signing_keys
100
+ jwk_key
101
+ else
102
+ raise 'id_token signing algorithm is currently not supported: %s' % policy.jwk_signing_algorithm
103
+ end
104
+ end
105
+
106
+ def jwk_key
107
+ key = policy.jwk_signing_keys
108
+ if key.has_key?('keys')
109
+ JSON::JWK::Set.new(key['keys']) # a set of keys
110
+ else
111
+ JSON::JWK.new(key) # a single key
112
+ end
113
+ end
114
+
115
+ def validate_id_token(seconds_since_epoc = Time.now.to_i)
116
+ JwtValidator.validate(id_token.raw_attributes, public_key, policy, seconds_since_epoc)
117
+ end
118
+
119
+ end # AuthenticationResponse
120
+ end # AzureActiveDirectoryB2C
121
+ end # Strategies
122
+ end # OmniAuth
@@ -0,0 +1,14 @@
1
+ require 'openid_connect'
2
+
3
+ module OmniAuth
4
+ module Strategies
5
+ class AzureActiveDirectoryB2C
6
+
7
+ class Client < ::OpenIDConnect::Client
8
+ # developers can override this class as required
9
+ # Be sure to also override MicrosoftAzureB2C::Policy#initialize_client
10
+ end
11
+
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,93 @@
1
+ module OmniAuth
2
+ module Strategies
3
+ class AzureActiveDirectoryB2C
4
+
5
+ class ValidationResult
6
+ def error_messages
7
+ @error_messages ||= []
8
+ end
9
+
10
+ def add_error(key, message)
11
+ error_messages << { error: key, message: message }
12
+ end
13
+
14
+ def has_errors?
15
+ error_messages.any?
16
+ end
17
+
18
+ def okay?
19
+ error_messages.empty?
20
+ end
21
+
22
+ def full_messages
23
+ error_messages.collect {|err| err[:message] }
24
+ end
25
+ end # ValidationResult
26
+
27
+ class JwtValidator
28
+
29
+ EPOC_TIME_LEEWAY_SECONDS = 10
30
+
31
+ attr_reader :jwt, :jwt_key, :policy, :seconds_since_epoc
32
+
33
+ def self.validate(jwt, jwt_key, policy, seconds_since_epoc = Time.now.to_i)
34
+ new(jwt, jwt_key, policy, seconds_since_epoc).validate
35
+ end
36
+
37
+ def initialize(jwt, jwt_key, policy, seconds_since_epoc = Time.now.to_i)
38
+ @jwt = jwt
39
+ @jwt_key = jwt_key
40
+ @policy = policy
41
+ @seconds_since_epoc = seconds_since_epoc
42
+ end
43
+
44
+ def validate
45
+ results = ValidationResult.new
46
+ results.add_error(:algorithm_mismatch, "Signing algorithm mismatch: Expected `#{policy.jwk_signing_algorithm} but got #{jwt.algorithm}`") unless signing_algoritm_matches?
47
+ results.add_error(:issue_mismatch, "Issue mismatch: Expected `#{policy.issuer}` but got `#{jwt[:iss]}`") unless issuer_matches?
48
+ results.add_error(:audience_mismatch, "Audience mismatch: Expected `#{policy.aud}` but got `#{jwt[:aud]}`") unless audience_matches?
49
+ results.add_error(:before_start_time, "Token has not yet commenced: Valid at #{jwt[:nbf]} but currently #{seconds_since_epoc}") unless on_or_after_not_before_time?
50
+ results.add_error(:after_expiry_time, "Token has expired: Expired at #{jwt[:exp]} but currently #{seconds_since_epoc}") unless before_expiration_time?
51
+
52
+ begin
53
+ verify_signature!
54
+ rescue JSON::JWS::VerificationFailed
55
+ results.add_error(:signiture_verification_failed, 'Signture verification failed') unless signature_verified?
56
+ rescue JSON::JWS::UnexpectedAlgorithm
57
+ results.add_error(:unexpected_signiture_algorithm, 'Unexpected signature algorithm') unless signature_verified?
58
+ rescue => e
59
+ results.add_error(:signiture_verification_failed, e.message || 'Signature verification failed') unless signature_verified?
60
+ end
61
+
62
+ results # return results
63
+ end
64
+
65
+ def signing_algoritm_matches?
66
+ # An attacker may change the signing algorith to provide a forged signature
67
+ jwt.algorithm.to_sym == policy.jwk_signing_algorithm
68
+ end
69
+
70
+ def issuer_matches?
71
+ jwt[:iss] && jwt[:iss] != '' && jwt[:iss] == policy.issuer
72
+ end
73
+
74
+ def audience_matches?
75
+ jwt[:aud] && jwt[:aud] != '' && jwt[:aud] == policy.application_identifier
76
+ end
77
+
78
+ def on_or_after_not_before_time?
79
+ (seconds_since_epoc + EPOC_TIME_LEEWAY_SECONDS) >= jwt[:nbf]
80
+ end
81
+
82
+ def before_expiration_time?
83
+ (seconds_since_epoc - EPOC_TIME_LEEWAY_SECONDS) < jwt[:exp]
84
+ end
85
+
86
+ def verify_signature!
87
+ jwt.verify!(jwt_key)
88
+ end
89
+
90
+ end # JwtVerifier
91
+ end # AzureActiveDirectoryB2C
92
+ end # Strategies
93
+ end # OmniAuth
@@ -0,0 +1,26 @@
1
+ module OmniAuth
2
+ module Strategies
3
+ class AzureActiveDirectoryB2C
4
+ class Policy
5
+ include AzureActiveDirectoryB2C::PolicyOptions
6
+
7
+ attr_reader :application_identifier, :application_secret, :issuer, :tenant_name, :policy_name, :jwk_signing_algorithm, :jwk_signing_keys
8
+
9
+ def initialize(application_identifier:, application_secret:, issuer:, tenant_name:, policy_name:, jwk_signing_algorithm:, jwk_signing_keys:, scope: nil)
10
+ @application_identifier = application_identifier
11
+ @application_secret = application_secret
12
+ @issuer = issuer
13
+ @tenant_name = tenant_name
14
+ @policy_name = policy_name
15
+ @jwk_signing_algorithm = jwk_signing_algorithm
16
+ @jwk_signing_keys = jwk_signing_keys
17
+ @scope = *scope
18
+ end
19
+
20
+ def scope
21
+ @scope.any? ? @scope : super
22
+ end
23
+ end # Policy
24
+ end # AzureActiveDirectoryB2C
25
+ end # Strategies
26
+ end # OmniAuth
@@ -0,0 +1,97 @@
1
+ module OmniAuth
2
+ module Strategies
3
+ class AzureActiveDirectoryB2C
4
+ module PolicyOptions
5
+
6
+ def respond_to_missing?(method_name, *args)
7
+ self.class.instance_methods.include?("policy_#{method_name}".to_sym) || super
8
+ end
9
+
10
+ def method_missing(method_name, *args, &block)
11
+ policy_method_name = 'policy_%s' % method_name
12
+ if respond_to?(policy_method_name)
13
+ send(policy_method_name, *args, &block)
14
+ else
15
+ super
16
+ end
17
+ end
18
+
19
+ def policy_application_identifier
20
+ raise MissingOptionError, '`application_identifier` not defined'
21
+ end
22
+
23
+ def policy_application_secret
24
+ raise MissingOptionError, '`application_secret` not defined'
25
+ end
26
+
27
+ def policy_issuer
28
+ raise MissingOptionError, '`issuer` not defined'
29
+ end
30
+
31
+ def policy_tenant_name
32
+ raise MissingOptionError, '`tenant_name` not defined'
33
+ end
34
+
35
+ def policy_policy_name
36
+ raise MissingOptionError, '`policy_name` not defined'
37
+ end
38
+
39
+ def policy_host_name
40
+ 'https://login.microsoftonline.com/te/%s/%s' % [tenant_name, policy_name]
41
+ end
42
+
43
+ def policy_authorization_endpoint
44
+ '%s/oauth2/v2.0/authorize' % host_name
45
+ end
46
+
47
+ def policy_token_endpoint
48
+ '%s/oauth2/v2.0/token' % host_name
49
+ end
50
+
51
+ def policy_jwks_uri
52
+ '%s/discovery/v2.0/keys' % host_name
53
+ end
54
+
55
+ def policy_jwk_signing_algorithm
56
+ # this can be "discovered" from the `jwks_uri` property at .well-known/openid-configuration
57
+ 'RS256'.to_sym
58
+ end
59
+
60
+ def policy_id_token_signing_algorithm
61
+ policy_jwk_signing_algorithm
62
+ end
63
+
64
+ def policy_scope
65
+ [
66
+ :openid, # This requests an ID token
67
+ # :offline_access, # This requests a refresh token using Auth Code flows. See: https://docs.microsoft.com/en-us/azure/active-directory-b2c/active-directory-b2c-reference-oauth-code).
68
+ # Request API permissions. See https://docs.microsoft.com/en-us/azure/active-directory-b2c/active-directory-b2c-access-tokens
69
+ ]
70
+ end
71
+
72
+ def policy_jwk_signing_keys
73
+ # public keys are listed at the url specified in the the `jwks_uri` property at .well-known/openid-configuration
74
+ # eg. https://login.microsoftonline.com/mipwtest.onmicrosoft.com/discovery/v2.0/keys?p=b2c_1_signupin
75
+ raise MissingOptionError, '`jwk_signing_keys` not defined'
76
+ end
77
+
78
+ def policy_default_client_options
79
+ {
80
+ identifier: application_identifier,
81
+ secret: application_secret,
82
+ authorization_endpoint: authorization_endpoint,
83
+ token_endpoint: token_endpoint,
84
+ jwks_uri: jwks_uri,
85
+ }
86
+ end
87
+
88
+ def policy_initialize_client(redirect_uri:, **override_options)
89
+ options = default_client_options.merge(override_options)
90
+ options[:redirect_uri] = redirect_uri
91
+ Client.new(options)
92
+ end
93
+
94
+ end # PolicyOptions
95
+ end # AzureActiveDirectoryB2C
96
+ end # Strategies
97
+ end # OmniAuth
@@ -0,0 +1,9 @@
1
+ module OmniAuth
2
+ module Strategies
3
+ class AzureActiveDirectoryB2C
4
+
5
+ VERSION = '0.2.0'
6
+
7
+ end
8
+ end
9
+ end
metadata ADDED
@@ -0,0 +1,96 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: omniauth-azure_active_directory_b2c
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ platform: ruby
6
+ authors:
7
+ - Brent Jacobs
8
+ - NextFaze
9
+ - Meeco
10
+ autorequire:
11
+ bindir: bin
12
+ cert_chain: []
13
+ date: 2017-10-27 00:00:00.000000000 Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: omniauth
17
+ requirement: !ruby/object:Gem::Requirement
18
+ requirements:
19
+ - - "~>"
20
+ - !ruby/object:Gem::Version
21
+ version: '1.3'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - "~>"
27
+ - !ruby/object:Gem::Version
28
+ version: '1.3'
29
+ - !ruby/object:Gem::Dependency
30
+ name: openid_connect
31
+ requirement: !ruby/object:Gem::Requirement
32
+ requirements:
33
+ - - "~>"
34
+ - !ruby/object:Gem::Version
35
+ version: '1.1'
36
+ type: :runtime
37
+ prerelease: false
38
+ version_requirements: !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - "~>"
41
+ - !ruby/object:Gem::Version
42
+ version: '1.1'
43
+ - !ruby/object:Gem::Dependency
44
+ name: proc_evaluate
45
+ requirement: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - "~>"
48
+ - !ruby/object:Gem::Version
49
+ version: '1.0'
50
+ type: :runtime
51
+ prerelease: false
52
+ version_requirements: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - "~>"
55
+ - !ruby/object:Gem::Version
56
+ version: '1.0'
57
+ description:
58
+ email: developers@meeco.me
59
+ executables: []
60
+ extensions: []
61
+ extra_rdoc_files: []
62
+ files:
63
+ - lib/omniauth-azure_active_directory_b2c.rb
64
+ - lib/omniauth/strategies/azure_active_directory_b2c.rb
65
+ - lib/omniauth/strategies/azure_active_directory_b2c/authentication_request.rb
66
+ - lib/omniauth/strategies/azure_active_directory_b2c/authentication_response.rb
67
+ - lib/omniauth/strategies/azure_active_directory_b2c/client.rb
68
+ - lib/omniauth/strategies/azure_active_directory_b2c/jwt_validator.rb
69
+ - lib/omniauth/strategies/azure_active_directory_b2c/policy.rb
70
+ - lib/omniauth/strategies/azure_active_directory_b2c/policy_options.rb
71
+ - lib/omniauth/strategies/azure_active_directory_b2c/version.rb
72
+ homepage: https://github.com/Meeco/omniauth-azure_active_directory_b2c
73
+ licenses:
74
+ - MIT
75
+ metadata: {}
76
+ post_install_message:
77
+ rdoc_options: []
78
+ require_paths:
79
+ - lib
80
+ required_ruby_version: !ruby/object:Gem::Requirement
81
+ requirements:
82
+ - - ">="
83
+ - !ruby/object:Gem::Version
84
+ version: '0'
85
+ required_rubygems_version: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ requirements: []
91
+ rubyforge_project:
92
+ rubygems_version: 2.6.12
93
+ signing_key:
94
+ specification_version: 4
95
+ summary: Azure AD B2C Strategy for OmniAuth.
96
+ test_files: []