Class: OmniAuth::Strategies::EntraId

Inherits:
OAuth2
  • Object
show all
Defined in:
lib/omniauth/strategies/entra_id.rb

Constant Summary collapse

BASE_URL =
'https://login.microsoftonline.com'
DEFAULT_SCOPE =
'openid profile email'
COMMON_TENANT_ID =
'common'
AD_FS_TENANT_ID =
'adfs'
ORGANIZATIONS_TENANT_ID =
'organizations'
CONSUMERS_TENANT_ID =
'consumers'
CONSUMERS_TENANT_GUID =
'9188040d-6c67-4c5b-b112-36a304b66dad'

Instance Method Summary collapse

Instance Method Details

#callback_urlObject



138
139
140
# File 'lib/omniauth/strategies/entra_id.rb', line 138

def callback_url
  full_host + callback_path
end

#clientObject



27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
# File 'lib/omniauth/strategies/entra_id.rb', line 27

def client
  provider = if options.tenant_provider
    options.tenant_provider.new(self)
  else
    options
  end

  options.client_id = provider.client_id

  if provider.respond_to?(:client_secret) && provider.client_secret
    options.client_secret = provider.client_secret
  elsif provider.respond_to?(:certificate_path) && provider.respond_to?(:tenant_id) && provider.certificate_path && provider.tenant_id
    options.token_params = {
      tenant:                provider.tenant_id,
      client_id:             provider.client_id,
      client_assertion:      client_assertion(provider.tenant_id, provider.client_id, provider.certificate_path),
      client_assertion_type: client_assertion_type
    }
  else
    raise ArgumentError, "You must provide either client_secret or certificate_path and tenant_id"
  end

  options.tenant_id = if provider.respond_to?(:tenant_id)
    provider.tenant_id
  else
    COMMON_TENANT_ID
  end

  options.base_url = if provider.respond_to?(:base_url )
    provider.base_url
  else
    BASE_URL
  end

  options.tenant_name                  = provider.tenant_name      if provider.respond_to?(:tenant_name)
  options.custom_policy                = provider.custom_policy    if provider.respond_to?(:custom_policy)
  options.authorize_params             = provider.authorize_params if provider.respond_to?(:authorize_params)
  options.authorize_params.domain_hint = provider.domain_hint      if provider.respond_to?(:domain_hint) && provider.domain_hint
  options.ignore_tid                   = provider.ignore_tid?      if provider.respond_to?(:ignore_tid?) && provider.ignore_tid?
  options.authorize_params.prompt      = request.params['prompt']  if defined?(request) && request.params['prompt']

  options.authorize_params.scope = if defined?(request) && request.params['scope']
    request.params['scope']
  elsif provider.respond_to?(:scope) && provider.scope
    provider.scope
  else
    DEFAULT_SCOPE
  end

  oauth2 = if provider.respond_to?(:adfs?) && provider.adfs?
    'oauth2'
  else
    'oauth2/v2.0'
  end

  tenanted_endpoint_base_url = if options.custom_policy && options.tenant_name
    "https://#{options.tenant_name}.b2clogin.com/#{options.tenant_name}.onmicrosoft.com/#{options.custom_policy}"
  else
    "#{options.base_url}/#{options.tenant_id}"
  end

  options.client_options.authorize_url = "#{tenanted_endpoint_base_url}/#{oauth2}/authorize"
  options.client_options.token_url     = "#{tenanted_endpoint_base_url}/#{oauth2}/token"

  super
end

#client_assertion(tenant_id, client_id, certificate_path) ⇒ Object



227
228
229
230
231
232
233
234
235
236
237
# File 'lib/omniauth/strategies/entra_id.rb', line 227

def client_assertion(tenant_id, client_id, certificate_path)
  certificate_file         = OpenSSL::PKCS12.new(File.read(certificate_path))
  certificate_thumbprint ||= Digest::SHA1.digest(certificate_file.certificate.to_der)
  private_key              = OpenSSL::PKey::RSA.new(certificate_file.key)

  claims = client_assertion_claims(tenant_id, client_id)
  x5c    = Base64.strict_encode64(certificate_file.certificate.to_der)
  x5t    = Base64.strict_encode64(certificate_thumbprint)

  JWT.encode(claims, private_key, 'RS256', { 'x5c': [x5c], 'x5t': x5t })
end

#client_assertion_claims(tenant_id, client_id) ⇒ Object



215
216
217
218
219
220
221
222
223
224
225
# File 'lib/omniauth/strategies/entra_id.rb', line 215

def client_assertion_claims(tenant_id, client_id)
  {
    'aud' => "https://login.microsoftonline.com/#{tenant_id}/oauth2/v2.0/token",
    'exp' => Time.now.to_i + 300,
    'iss' => client_id,
    'jti' => SecureRandom.uuid,
    'nbf' => Time.now.to_i,
    'sub' => client_id,
    'iat' => Time.now.to_i
  }
end

#client_assertion_typeObject

https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-auth-code-flow#request-an-access-token-with-a-certificate-credential

The below methods support the flow for using certificate-based client assertion authentication.



211
212
213
# File 'lib/omniauth/strategies/entra_id.rb', line 211

def client_assertion_type
  'urn:ietf:params:oauth:client-assertion-type:jwt-bearer'
end

#raw_infoObject

https://learn.microsoft.com/en-us/entra/identity-platform/id-tokens

Some account types from Microsoft seem to only have a decodable ID token, with JWT unable to decode the access token. Information is limited in those cases. Other account types provide an expanded set of data inside the auth token, which does decode as a JWT.

Merge the two, allowing the expanded auth token data to overwrite the ID token data if keys collide, and use this as raw info.



152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
# File 'lib/omniauth/strategies/entra_id.rb', line 152

def raw_info
  if @raw_info.nil?
    id_token_data = begin
      ::JWT.decode(access_token.params['id_token'], nil, false).first
    rescue StandardError
      {}
    end

    # For multi-tenant apps (the 'common' tenant_id) it doesn't make any
    # sense to verify the token issuer, because the value of 'iss' in the
    # token depends on the 'tid' in the token itself. We should also skip
    # for AD FS local instances, as we don't put a valid tenant ID in its
    # place, but "adfs" (see AD_FS_TENANT_ID) instead.
    #
    # TODO: Unclear about approach to use for ORGANIZATIONS_TENANT_ID.
    #
    do_not_verify = (
      options.tenant_id.nil? ||
      options.tenant_id == COMMON_TENANT_ID ||
      options.tenant_id == AD_FS_TENANT_ID
    )

    issuer = if do_not_verify
      nil
    elsif options.tenant_id == CONSUMERS_TENANT_ID
      "#{options.base_url || BASE_URL}/#{CONSUMERS_TENANT_GUID}/v2.0"
    else
      "#{options.base_url || BASE_URL}/#{options.tenant_id}/v2.0"
    end

    # https://learn.microsoft.com/en-us/entra/identity-platform/id-tokens#validate-tokens
    #
    verify_params = {
      aud: options.client_id,
      exp: { leeway: options.jwt_leeway },
      nbf: { leeway: options.jwt_leeway }
    }
    verify_params[:iss] = issuer unless issuer.nil?

    ::JWT::Claims.verify_payload!(id_token_data, verify_params)

    auth_token_data = begin
      ::JWT.decode(access_token.token, nil, false).first
    rescue StandardError
      {}
    end

    id_token_data.merge!(auth_token_data)
    @raw_info = id_token_data
  end

  @raw_info
end