Class: RubyOidcClient::IDPartner

Inherits:
Object
  • Object
show all
Defined in:
lib/id_partner.rb

Constant Summary collapse

SUPPORTED_AUTH_METHODS =
[
  "client_secret_basic",
  "tls_client_auth",
  "private_key_jwt" # For backward compatibility
].freeze
SIGNING_ALG =
"PS256"
ENCRYPTION_ALG =
"RSA-OAEP"
ENCRYPTION_ENC =
"A256CBC-HS512"

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(config) ⇒ IDPartner

Returns a new instance of IDPartner.

Raises:

  • (ArgumentError)


22
23
24
25
26
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
# File 'lib/id_partner.rb', line 22

def initialize(config)
  raise ArgumentError, "Config missing." unless config

  default_config = {
    account_selector_service_url: "https://auth-api.idpartner.com/oidc-proxy",
    token_endpoint_auth_method: "client_secret_basic",
    jwks: nil,
    client_secret: nil
  }

  @config = default_config.merge(config)

  unless SUPPORTED_AUTH_METHODS.include?(@config[:token_endpoint_auth_method])
    raise ArgumentError,
          "Unsupported token_endpoint_auth_method '#{config[:token_endpoint_auth_method]}'. It must be one of (#{SUPPORTED_AUTH_METHODS.join(", ")})"
  end

  client_secret_config = @config[:token_endpoint_auth_method] == "client_secret_basic" ? { client_secret: @config[:client_secret] } : {}

  jwks_config = if @config[:jwks]
                  {
                    authorization_encrypted_response_alg: ENCRYPTION_ALG,
                    authorization_encrypted_response_enc: ENCRYPTION_ENC,
                    id_token_encrypted_response_alg: ENCRYPTION_ALG,
                    id_token_encrypted_response_enc: ENCRYPTION_ENC,
                    request_object_signing_alg: SIGNING_ALG
                  }
                else
                  {}
                end

  @config = @config.merge({
                            authorization_signed_response_alg: SIGNING_ALG,
                            id_token_signed_response_alg: SIGNING_ALG
                          }).merge(client_secret_config).merge(jwks_config)
end

Instance Attribute Details

#configObject (readonly)

Returns the value of attribute config.



20
21
22
# File 'lib/id_partner.rb', line 20

def config
  @config
end

#endpointsObject (readonly)

Returns the value of attribute endpoints.



20
21
22
# File 'lib/id_partner.rb', line 20

def endpoints
  @endpoints
end

#provider_keysObject (readonly)

Returns the value of attribute provider_keys.



20
21
22
# File 'lib/id_partner.rb', line 20

def provider_keys
  @provider_keys
end

Instance Method Details

#generate_proofsObject



59
60
61
62
63
64
65
# File 'lib/id_partner.rb', line 59

def generate_proofs
  {
    state: SecureRandom.urlsafe_base64(64),
    nonce: SecureRandom.urlsafe_base64(64),
    code_verifier: SecureRandom.urlsafe_base64(64)
  }
end

#get_authorization_url(query, proofs, scope, extra_authorization_params = {}) ⇒ Object

Raises:

  • (ArgumentError)


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
93
94
95
96
97
98
99
100
101
# File 'lib/id_partner.rb', line 67

def get_authorization_url(query, proofs, scope, extra_authorization_params = {})
  raise ArgumentError, "The URL query parameter is required." unless query
  raise ArgumentError, "The scope parameter is required." unless scope
  raise ArgumentError, "The proofs parameter is required." unless proofs

  if query[:iss].nil?
    return "#{config[:account_selector_service_url]}/auth/select-accounts?client_id=#{config[:client_id]}&visitor_id=#{query[:visitor_id]}&scope=#{scope}&claims=#{extract_claims(extra_authorization_params[:claims]).join("+")}"
  end

  @config[:iss] = query[:iss]
  obtain_well_known_config_endpoints

  extra_authorization_params[:claims] = extra_authorization_params[:claims]&.to_json
  extended_authorization_params = {
    redirect_uri: config[:redirect_uri],
    code_challenge_method: "S256",
    code_challenge: generate_code_challenge(proofs[:code_verifier]),
    state: proofs[:state],
    nonce: proofs[:nonce],
    scope: scope,
    response_type: "code",
    client_id: config[:client_id],
    'x-fapi-interaction-id': SecureRandom.uuid,
    identity_provider_id: query[:idp_id],
    idpartner_token: query[:idpartner_token],
    response_mode: "jwt"
  }.merge(extra_authorization_params).compact

  pushed_authorization_request_params = extended_authorization_params
  pushed_authorization_request_params = { request: create_request_object(extended_authorization_params) } if config[:jwks]

  request_uri = push_authorization_request(pushed_authorization_request_params)["request_uri"]
  query_params = URI.encode_www_form(request_uri: request_uri)
  "#{endpoints[:authorization_endpoint]}?#{query_params}"
end

#public_jwksObject



103
104
105
106
107
108
109
110
111
112
113
# File 'lib/id_partner.rb', line 103

def public_jwks
  return {} unless config[:jwks]

  jwk_set = JSON::JWK::Set.new(JSON.parse(config[:jwks]))
  public_jwks = jwk_set.collect do |jwk|
    public_jwk = jwk.to_key.public_key.to_jwk
    public_jwk.merge("alg" => jwk["alg"], "use" => jwk["use"]).compact
  end

  { "keys" => public_jwks }
end

#token(query, proofs) ⇒ Object



115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
# File 'lib/id_partner.rb', line 115

def token(query, proofs)
  decoded_jwt = decode_jwt(query[:response])
  basic_auth_credentials = Base64.strict_encode64("#{config[:client_id]}:#{config[:client_secret]}")
  payload = {
    code: decoded_jwt["code"],
    code_verifier: proofs[:code_verifier],
    grant_type: "authorization_code",
    redirect_uri: config[:redirect_uri]
  }

  uri = URI(endpoints[:token_endpoint])
  http = Net::HTTP.new(uri.host, uri.port)
  headers = {
    "Authorization" => "Basic #{basic_auth_credentials}",
    "Content-Type" => "application/x-www-form-urlencoded",
    "Accept" => "application/json"
  }
  token_request = Net::HTTP::Post.new(uri.request_uri, headers)
  token_request.set_form_data(payload)
  token_response = http.request(token_request)
  raise "Failed to exchange token: #{token_response.body}" unless token_response.is_a?(Net::HTTPSuccess)

  JSON.parse(token_response.body)
end

#userinfo(access_token) ⇒ Object



140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
# File 'lib/id_partner.rb', line 140

def userinfo(access_token)
  uri = URI(endpoints[:userinfo_endpoint])
  http = Net::HTTP.new(uri.host, uri.port)
  headers = {
    "Authorization" => "Bearer #{access_token}",
    "Accept" => "application/json"
  }

  userinfo_request = Net::HTTP::Get.new(uri.request_uri, headers)
  userinfo_response = http.request(userinfo_request)

  raise "Failed to retrieve userinfo: #{userinfo_response.body}" unless userinfo_response.is_a?(Net::HTTPSuccess)

  JSON.parse(userinfo_response.body)
rescue StandardError => e
  raise "Failed to fetch well-known config: #{e.message}"
end