omniauth_oidc 0.2.7 → 1.0.1
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.
- checksums.yaml +4 -4
- data/.rubocop.yml +2 -1
- data/CHANGELOG.md +74 -0
- data/LICENSE.txt +1 -1
- data/README.md +125 -63
- data/lib/omniauth/oidc/client.rb +86 -0
- data/lib/omniauth/oidc/config_fetcher.rb +76 -0
- data/lib/omniauth/oidc/errors.rb +20 -1
- data/lib/omniauth/oidc/http_client.rb +81 -0
- data/lib/omniauth/oidc/jwk_handler.rb +48 -0
- data/lib/omniauth/oidc/jwks_cache.rb +89 -0
- data/lib/omniauth/oidc/logging.rb +76 -0
- data/lib/omniauth/oidc/response_objects.rb +176 -0
- data/lib/omniauth/oidc/version.rb +1 -1
- data/lib/omniauth/strategies/oidc/callback.rb +119 -82
- data/lib/omniauth/strategies/oidc/request.rb +20 -16
- data/lib/omniauth/strategies/oidc/verify.rb +118 -68
- data/lib/omniauth/strategies/oidc.rb +70 -31
- data/lib/omniauth_oidc.rb +7 -0
- data/omniauth_oidc.gemspec +7 -5
- data/sig/omniauth_oidc.rbs +192 -1
- metadata +22 -36
data/lib/omniauth/oidc/errors.rb
CHANGED
|
@@ -3,7 +3,26 @@
|
|
|
3
3
|
module OmniauthOidc
|
|
4
4
|
class Error < RuntimeError; end
|
|
5
5
|
|
|
6
|
+
# Authentication flow errors
|
|
6
7
|
class MissingCodeError < Error; end
|
|
7
|
-
|
|
8
8
|
class MissingIdTokenError < Error; end
|
|
9
|
+
|
|
10
|
+
# Configuration errors
|
|
11
|
+
class ConfigurationError < Error; end
|
|
12
|
+
class MissingConfigurationError < ConfigurationError; end
|
|
13
|
+
|
|
14
|
+
# Token/JWT errors
|
|
15
|
+
class TokenError < Error; end
|
|
16
|
+
class TokenVerificationError < TokenError; end
|
|
17
|
+
class TokenExpiredError < TokenError; end
|
|
18
|
+
class InvalidAlgorithmError < TokenError; end
|
|
19
|
+
class InvalidSignatureError < TokenError; end
|
|
20
|
+
class InvalidIssuerError < TokenError; end
|
|
21
|
+
class InvalidAudienceError < TokenError; end
|
|
22
|
+
class InvalidNonceError < TokenError; end
|
|
23
|
+
|
|
24
|
+
# JWKS errors
|
|
25
|
+
class JwksError < Error; end
|
|
26
|
+
class JwksFetchError < JwksError; end
|
|
27
|
+
class KeyNotFoundError < JwksError; end
|
|
9
28
|
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "uri"
|
|
5
|
+
require "json"
|
|
6
|
+
|
|
7
|
+
module OmniauthOidc
|
|
8
|
+
# Simple HTTP client using Net::HTTP
|
|
9
|
+
class HttpClient
|
|
10
|
+
class HttpError < StandardError; end
|
|
11
|
+
|
|
12
|
+
MAX_REDIRECTS = 5
|
|
13
|
+
|
|
14
|
+
def self.get(url, headers: {})
|
|
15
|
+
uri = URI.parse(url)
|
|
16
|
+
request = Net::HTTP::Get.new(uri)
|
|
17
|
+
headers.each { |key, value| request[key] = value }
|
|
18
|
+
|
|
19
|
+
response = execute_request(uri, request)
|
|
20
|
+
handle_response(response, url, headers: headers, redirects_remaining: MAX_REDIRECTS)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.post(url, body: nil, headers: {})
|
|
24
|
+
uri = URI.parse(url)
|
|
25
|
+
request = Net::HTTP::Post.new(uri)
|
|
26
|
+
request["Content-Type"] = "application/x-www-form-urlencoded" unless headers["Content-Type"]
|
|
27
|
+
headers.each { |key, value| request[key] = value }
|
|
28
|
+
request.body = body if body
|
|
29
|
+
|
|
30
|
+
response = execute_request(uri, request)
|
|
31
|
+
handle_response(response, url)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def self.execute_request(uri, request)
|
|
35
|
+
OmniauthOidc::Logging.instrument("http.request", method: request.method, uri: uri.to_s) do
|
|
36
|
+
Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == "https") do |http|
|
|
37
|
+
http.request(request)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
private_class_method :execute_request
|
|
42
|
+
|
|
43
|
+
def self.handle_response(response, url, headers: {}, redirects_remaining: 0)
|
|
44
|
+
case response
|
|
45
|
+
when Net::HTTPSuccess
|
|
46
|
+
parse_json_response(response.body)
|
|
47
|
+
when Net::HTTPRedirection
|
|
48
|
+
raise HttpError, "Too many redirects for #{url}" if redirects_remaining <= 0
|
|
49
|
+
|
|
50
|
+
location = response["location"]
|
|
51
|
+
raise HttpError, "HTTP redirect from #{url} with no location header" unless location
|
|
52
|
+
|
|
53
|
+
redirect_uri = URI.parse(location)
|
|
54
|
+
# Resolve relative redirects
|
|
55
|
+
redirect_uri = URI.join(url, location) unless redirect_uri.host
|
|
56
|
+
|
|
57
|
+
OmniauthOidc::Logging.debug("Following redirect", from: url, to: redirect_uri.to_s)
|
|
58
|
+
follow_redirect(redirect_uri, headers: headers, redirects_remaining: redirects_remaining - 1)
|
|
59
|
+
else
|
|
60
|
+
raise HttpError, "HTTP request failed: #{response.code} #{response.message} for #{url}"
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
private_class_method :handle_response
|
|
64
|
+
|
|
65
|
+
def self.follow_redirect(uri, headers: {}, redirects_remaining: 0)
|
|
66
|
+
request = Net::HTTP::Get.new(uri)
|
|
67
|
+
headers.each { |key, value| request[key] = value }
|
|
68
|
+
|
|
69
|
+
response = execute_request(uri, request)
|
|
70
|
+
handle_response(response, uri.to_s, headers: headers, redirects_remaining: redirects_remaining)
|
|
71
|
+
end
|
|
72
|
+
private_class_method :follow_redirect
|
|
73
|
+
|
|
74
|
+
def self.parse_json_response(body)
|
|
75
|
+
JSON.parse(body)
|
|
76
|
+
rescue JSON::ParserError => e
|
|
77
|
+
raise HttpError, "Failed to parse JSON response: #{e.message}"
|
|
78
|
+
end
|
|
79
|
+
private_class_method :parse_json_response
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "jwt"
|
|
4
|
+
require "openssl"
|
|
5
|
+
|
|
6
|
+
module OmniauthOidc
|
|
7
|
+
# Handles JWK/JWKS operations for JWT verification
|
|
8
|
+
class JwkHandler
|
|
9
|
+
# Parsed key with its kid for matching
|
|
10
|
+
KeyWithId = Struct.new(:kid, :keypair, keyword_init: true)
|
|
11
|
+
|
|
12
|
+
def self.parse_jwks(jwks_data)
|
|
13
|
+
return nil unless jwks_data
|
|
14
|
+
|
|
15
|
+
jwks_data = JSON.parse(jwks_data) if jwks_data.is_a?(String)
|
|
16
|
+
|
|
17
|
+
# Handle JWKS (set of keys)
|
|
18
|
+
if jwks_data["keys"]
|
|
19
|
+
jwks_data["keys"].filter_map { |key_data| jwk_to_key(key_data) }
|
|
20
|
+
# Handle single JWK
|
|
21
|
+
else
|
|
22
|
+
[jwk_to_key(jwks_data)].compact
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.jwk_to_key(jwk_data)
|
|
27
|
+
keypair = JWT::JWK.import(jwk_data).keypair
|
|
28
|
+
kid = jwk_data["kid"] || jwk_data[:kid]
|
|
29
|
+
KeyWithId.new(kid: kid, keypair: keypair)
|
|
30
|
+
rescue StandardError => e
|
|
31
|
+
OmniauthOidc::Logging.error("Failed to import JWK", error: e.message)
|
|
32
|
+
nil
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Find the right key from JWKS based on kid (key ID)
|
|
36
|
+
def self.find_key(keys, kid = nil)
|
|
37
|
+
return keys.first&.keypair if kid.nil? || keys.size <= 1
|
|
38
|
+
|
|
39
|
+
matched = keys.find { |k| k.kid == kid }
|
|
40
|
+
if matched
|
|
41
|
+
matched.keypair
|
|
42
|
+
else
|
|
43
|
+
OmniauthOidc::Logging.warn("No JWK found matching kid '#{kid}', falling back to first key")
|
|
44
|
+
keys.first&.keypair
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OmniauthOidc
|
|
4
|
+
# Thread-safe JWKS cache with configurable TTL and force refresh capability
|
|
5
|
+
class JwksCache
|
|
6
|
+
DEFAULT_TTL = 3600 # 1 hour in seconds
|
|
7
|
+
|
|
8
|
+
CacheEntry = Struct.new(:keys, :fetched_at, keyword_init: true)
|
|
9
|
+
|
|
10
|
+
class << self
|
|
11
|
+
def instance
|
|
12
|
+
@instance ||= new
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Clear all cached keys (useful for testing)
|
|
16
|
+
def clear!
|
|
17
|
+
instance.clear!
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Force refresh a specific JWKS URI on next access
|
|
21
|
+
def invalidate(jwks_uri)
|
|
22
|
+
instance.invalidate(jwks_uri)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def initialize(ttl: DEFAULT_TTL)
|
|
27
|
+
@ttl = ttl
|
|
28
|
+
@cache = {}
|
|
29
|
+
@mutex = Mutex.new
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Fetch JWKS, using cache if valid
|
|
33
|
+
def fetch(jwks_uri, force_refresh: false, &block)
|
|
34
|
+
@mutex.synchronize do
|
|
35
|
+
entry = @cache[jwks_uri]
|
|
36
|
+
|
|
37
|
+
if !force_refresh && entry && !expired?(entry)
|
|
38
|
+
Logging.debug("JWKS cache hit", jwks_uri: jwks_uri)
|
|
39
|
+
return entry.keys
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
Logging.info("JWKS cache miss, fetching", jwks_uri: jwks_uri, force_refresh: force_refresh)
|
|
43
|
+
keys = block.call
|
|
44
|
+
@cache[jwks_uri] = CacheEntry.new(keys: keys, fetched_at: Time.now)
|
|
45
|
+
keys
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Check if a specific URI's cache is valid
|
|
50
|
+
def valid?(jwks_uri)
|
|
51
|
+
@mutex.synchronize do
|
|
52
|
+
entry = @cache[jwks_uri]
|
|
53
|
+
entry && !expired?(entry)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Invalidate a specific URI's cache
|
|
58
|
+
def invalidate(jwks_uri)
|
|
59
|
+
@mutex.synchronize do
|
|
60
|
+
@cache.delete(jwks_uri)
|
|
61
|
+
Logging.debug("JWKS cache invalidated", jwks_uri: jwks_uri)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Clear entire cache
|
|
66
|
+
def clear!
|
|
67
|
+
@mutex.synchronize do
|
|
68
|
+
@cache.clear
|
|
69
|
+
Logging.debug("JWKS cache cleared")
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Get current TTL
|
|
74
|
+
attr_reader :ttl
|
|
75
|
+
|
|
76
|
+
# Update TTL (affects future expiration checks)
|
|
77
|
+
def ttl=(value)
|
|
78
|
+
@mutex.synchronize do
|
|
79
|
+
@ttl = value
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
private
|
|
84
|
+
|
|
85
|
+
def expired?(entry)
|
|
86
|
+
Time.now - entry.fetched_at > @ttl
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "logger"
|
|
4
|
+
|
|
5
|
+
module OmniauthOidc
|
|
6
|
+
# Logging module providing both Ruby Logger and ActiveSupport::Notifications support
|
|
7
|
+
module Logging
|
|
8
|
+
class << self
|
|
9
|
+
attr_writer :logger
|
|
10
|
+
|
|
11
|
+
def logger
|
|
12
|
+
@logger ||= default_logger
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def log_level=(level)
|
|
16
|
+
logger.level = level
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def instrument(event_name, payload = {}, &block)
|
|
20
|
+
full_event_name = "#{event_name}.omniauth_oidc"
|
|
21
|
+
|
|
22
|
+
# Always log the event
|
|
23
|
+
log_event(event_name, payload)
|
|
24
|
+
|
|
25
|
+
# Use ActiveSupport::Notifications if available
|
|
26
|
+
if defined?(ActiveSupport::Notifications)
|
|
27
|
+
ActiveSupport::Notifications.instrument(full_event_name, payload, &block)
|
|
28
|
+
elsif block
|
|
29
|
+
yield payload
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def debug(message, context = {})
|
|
34
|
+
log(:debug, message, context)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def info(message, context = {})
|
|
38
|
+
log(:info, message, context)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def warn(message, context = {})
|
|
42
|
+
log(:warn, message, context)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def error(message, context = {})
|
|
46
|
+
log(:error, message, context)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def default_logger
|
|
52
|
+
Logger.new($stdout).tap do |log|
|
|
53
|
+
log.progname = "OmniauthOidc"
|
|
54
|
+
log.level = Logger::WARN # Default to WARN to avoid noise
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def log(level, message, context)
|
|
59
|
+
formatted_message = context.empty? ? message : "#{message} #{context.inspect}"
|
|
60
|
+
logger.send(level, formatted_message)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def log_event(event_name, payload)
|
|
64
|
+
# Log at debug level for instrumentation events
|
|
65
|
+
sanitized_payload = sanitize_payload(payload)
|
|
66
|
+
debug("Event: #{event_name}", sanitized_payload)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def sanitize_payload(payload)
|
|
70
|
+
# Remove sensitive data from logs
|
|
71
|
+
sensitive_keys = %i[secret client_secret access_token id_token refresh_token code_verifier]
|
|
72
|
+
payload.reject { |k, _| sensitive_keys.include?(k.to_sym) }
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OmniauthOidc
|
|
4
|
+
# Simple response objects to replace OpenIDConnect::ResponseObject classes
|
|
5
|
+
module ResponseObjects
|
|
6
|
+
# Represents an OIDC ID Token with claims
|
|
7
|
+
class IdToken
|
|
8
|
+
attr_reader :raw_attributes
|
|
9
|
+
|
|
10
|
+
def initialize(attributes = {})
|
|
11
|
+
@raw_attributes = attributes.is_a?(Hash) ? attributes : attributes.to_h
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def sub
|
|
15
|
+
raw_attributes["sub"]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def iss
|
|
19
|
+
raw_attributes["iss"]
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def aud
|
|
23
|
+
raw_attributes["aud"]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def exp
|
|
27
|
+
raw_attributes["exp"]
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def iat
|
|
31
|
+
raw_attributes["iat"]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def nonce
|
|
35
|
+
raw_attributes["nonce"]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Allow method-style access to claims
|
|
39
|
+
def method_missing(method_name, *args)
|
|
40
|
+
return raw_attributes[method_name.to_s] if raw_attributes.key?(method_name.to_s)
|
|
41
|
+
|
|
42
|
+
super
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def respond_to_missing?(method_name, include_private = false)
|
|
46
|
+
raw_attributes.key?(method_name.to_s) || super
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Represents OIDC UserInfo response
|
|
51
|
+
class UserInfo
|
|
52
|
+
attr_reader :raw_attributes
|
|
53
|
+
|
|
54
|
+
def initialize(attributes = {})
|
|
55
|
+
@raw_attributes = attributes.is_a?(Hash) ? attributes : attributes.to_h
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Standard OIDC claims
|
|
59
|
+
def sub
|
|
60
|
+
raw_attributes["sub"]
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def name
|
|
64
|
+
raw_attributes["name"]
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def given_name
|
|
68
|
+
raw_attributes["given_name"]
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def family_name
|
|
72
|
+
raw_attributes["family_name"]
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def middle_name
|
|
76
|
+
raw_attributes["middle_name"]
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def nickname
|
|
80
|
+
raw_attributes["nickname"]
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def preferred_username
|
|
84
|
+
raw_attributes["preferred_username"]
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def profile
|
|
88
|
+
raw_attributes["profile"]
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def picture
|
|
92
|
+
raw_attributes["picture"]
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def website
|
|
96
|
+
raw_attributes["website"]
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def email
|
|
100
|
+
raw_attributes["email"]
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def email_verified
|
|
104
|
+
raw_attributes["email_verified"]
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def gender
|
|
108
|
+
raw_attributes["gender"]
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def birthdate
|
|
112
|
+
raw_attributes["birthdate"]
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def zoneinfo
|
|
116
|
+
raw_attributes["zoneinfo"]
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def locale
|
|
120
|
+
raw_attributes["locale"]
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def phone_number
|
|
124
|
+
raw_attributes["phone_number"]
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def phone_number_verified
|
|
128
|
+
raw_attributes["phone_number_verified"]
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def address
|
|
132
|
+
raw_attributes["address"]
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def updated_at
|
|
136
|
+
raw_attributes["updated_at"]
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Allow method-style access to custom claims
|
|
140
|
+
def method_missing(method_name, *args)
|
|
141
|
+
return raw_attributes[method_name.to_s] if raw_attributes.key?(method_name.to_s)
|
|
142
|
+
|
|
143
|
+
super
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def respond_to_missing?(method_name, include_private = false)
|
|
147
|
+
raw_attributes.key?(method_name.to_s) || super
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Represents an OAuth2 Access Token
|
|
152
|
+
class AccessToken
|
|
153
|
+
attr_reader :access_token, :token_type, :expires_in, :refresh_token, :scope, :id_token
|
|
154
|
+
|
|
155
|
+
def initialize(attributes = {})
|
|
156
|
+
@access_token = attributes["access_token"] || attributes[:access_token]
|
|
157
|
+
@token_type = attributes["token_type"] || attributes[:token_type]
|
|
158
|
+
@expires_in = attributes["expires_in"] || attributes[:expires_in]
|
|
159
|
+
@refresh_token = attributes["refresh_token"] || attributes[:refresh_token]
|
|
160
|
+
@scope = attributes["scope"] || attributes[:scope]
|
|
161
|
+
@id_token = attributes["id_token"] || attributes[:id_token]
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def to_h
|
|
165
|
+
{
|
|
166
|
+
"access_token" => access_token,
|
|
167
|
+
"token_type" => token_type,
|
|
168
|
+
"expires_in" => expires_in,
|
|
169
|
+
"refresh_token" => refresh_token,
|
|
170
|
+
"scope" => scope,
|
|
171
|
+
"id_token" => id_token
|
|
172
|
+
}.compact
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|