Module: OmniauthOpenidFederation::Tasks::CallbackProcessor

Defined in:
lib/omniauth_openid_federation/tasks/callback_processor.rb

Class Method Summary collapse

Class Method Details

.process(callback_url:, base_url:, client_id:, redirect_uri:, private_key:, entity_statement_url: nil, entity_statement_path: nil, provider_acr: nil, client_entity_statement_url: nil, client_entity_statement_path: nil) ⇒ Object



9
10
11
12
13
14
15
16
17
18
19
20
21
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
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
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
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
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
# File 'lib/omniauth_openid_federation/tasks/callback_processor.rb', line 9

def self.process(
  callback_url:,
  base_url:,
  client_id:, redirect_uri:, private_key:, entity_statement_url: nil,
  entity_statement_path: nil,
  provider_acr: nil,
  client_entity_statement_url: nil,
  client_entity_statement_path: nil
)
  require "uri"
  require "json"
  require "base64"
  require_relative "../strategy"

  results = {
    steps_completed: [],
    errors: [],
    warnings: [],
    compliance_checks: {},
    token_info: {},
    id_token_claims: {}
  }

  # Parse callback URL
  begin
    # Note: Rake tasks are developer tools, no security validation needed
    begin
      uri = URI.parse(callback_url)
    rescue URI::InvalidURIError => e
      raise "Invalid callback URL: #{e.message}"
    end
    pairs = URI.decode_www_form(uri.query || "")
    params = pairs.group_by(&:first).transform_values { |vs| vs.map(&:last) }

    auth_code = params["code"]&.first
    state = params["state"]&.first
    error = params["error"]&.first
    error_description = params["error_description"]&.first

    if error
      raise "Authorization error: #{error}#{" - #{error_description}" if error_description}"
    end

    unless auth_code
      raise "No authorization code found in callback URL"
    end

    results[:authorization_code] = auth_code
    results[:state] = state
    results[:steps_completed] << "parse_callback"
  rescue => e
    results[:errors] << "Callback parsing: #{e.message}"
    raise
  end

  # Build strategy options from provided parameters
  begin
    # Resolve entity statement URL if only path provided
    resolved_entity_statement_url = entity_statement_url
    if resolved_entity_statement_url.nil? && entity_statement_path
      # If only path provided, try to resolve from base_url
      resolved_entity_statement_url = "#{base_url}/.well-known/openid-federation"
    end

    # Resolve client entity statement URL if only path provided
    resolved_client_entity_statement_url = client_entity_statement_url
    if resolved_client_entity_statement_url.nil? && client_entity_statement_path
      resolved_client_entity_statement_url = "#{base_url}/.well-known/openid-federation"
    end

    # Build strategy options
    strategy_options = {
      discovery: true,
      scope: [:openid],
      response_type: "code",
      client_auth_method: :jwt_bearer,
      client_signing_alg: :RS256,
      always_encrypt_request_object: true,
      entity_statement_url: resolved_entity_statement_url,
      entity_statement_path: entity_statement_path,
      client_entity_statement_url: resolved_client_entity_statement_url,
      client_entity_statement_path: client_entity_statement_path,
      client_options: {
        identifier: client_id,
        redirect_uri: redirect_uri,
        private_key: private_key
      }
    }

    # Store client_auth_method before filtering nil values
    client_auth_method = strategy_options[:client_auth_method] || :jwt_bearer

    # Remove nil values
    strategy_options = strategy_options.reject { |_k, v| v.nil? }
    strategy_options[:client_options] = strategy_options[:client_options].reject { |_k, v| v.nil? }

    strategy = OmniAuth::Strategies::OpenIDFederation.new(nil, strategy_options)
    oidc_client = strategy.client

    unless oidc_client
      raise "Failed to initialize OpenID Connect client"
    end

    unless oidc_client.private_key
      raise "Private key not set on OpenID Connect client (required for private_key_jwt)"
    end

    results[:steps_completed] << "initialize_strategy"
  rescue => e
    results[:errors] << "Strategy initialization: #{e.message}"
    raise
  end

  # Exchange authorization code for tokens
  begin
    oidc_client.authorization_code = auth_code
    oidc_client.redirect_uri = redirect_uri
    access_token = oidc_client.access_token!(client_auth_method)

    id_token_raw = access_token.id_token
    access_token_value = access_token.access_token
    refresh_token = access_token.refresh_token

    results[:token_info] = {
      access_token: access_token_value ? "#{access_token_value[0..30]}..." : nil,
      refresh_token: refresh_token ? "Present" : "Not provided",
      id_token_encrypted: id_token_raw ? "#{id_token_raw[0..50]}..." : nil
    }

    results[:steps_completed] << "token_exchange"
  rescue => e
    results[:errors] << "Token exchange: #{e.message}"
    raise
  end

  # Decrypt and validate ID token
  begin
    id_token = strategy.send(:decode_id_token, id_token_raw)

    results[:id_token_claims] = {
      iss: id_token.iss,
      sub: id_token.sub,
      aud: id_token.aud,
      exp: id_token.exp,
      iat: id_token.iat,
      nonce: id_token.nonce,
      acr: id_token.acr,
      auth_time: id_token.auth_time,
      amr: id_token.amr
    }

    # Validate required claims
    required_claims = {
      iss: id_token.iss,
      sub: id_token.sub,
      aud: id_token.aud,
      exp: id_token.exp,
      iat: id_token.iat
    }

    missing_claims = required_claims.select { |_k, v| v.nil? }
    if missing_claims.empty?
      results[:id_token_valid] = true
    else
      results[:errors] << "Missing required ID token claims: #{missing_claims.keys.join(", ")}"
    end

    results[:steps_completed] << "id_token_validation"
  rescue => e
    results[:errors] << "ID token validation: #{e.message}"
    raise
  end

  # Validate OpenID Federation compliance
  results[:compliance_checks] = {
    "Signed Request Objects" => {
      status: "✅ MANDATORY",
      description: "All requests use signed request objects (RFC 9101)",
      verified: true
    },
    "ID Token Encryption" => {
      status: "✅ MANDATORY",
      description: "ID tokens are encrypted (RSA-OAEP + A128CBC-HS256)",
      verified: OmniauthOpenidFederation::Jwe.encrypted?(id_token_raw)
    },
    "Client Assertion (private_key_jwt)" => {
      status: "✅ MANDATORY",
      description: "Token endpoint uses private_key_jwt authentication",
      verified: true
    },
    "Entity Statement JWKS" => {
      status: "✅ MANDATORY",
      description: "JWKS extracted from entity statement",
      verified: StringHelpers.present?(entity_statement_path) || StringHelpers.present?(entity_statement_url)
    },
    "Signed JWKS Support" => {
      status: "✅ MANDATORY",
      description: "Supports OpenID Federation signed JWKS for key rotation",
      verified: true
    }
  }

  # Check for client entity statement (optional but recommended)
  if StringHelpers.present?(client_entity_statement_path) || StringHelpers.present?(client_entity_statement_url)
    results[:compliance_checks]["Client Entity Statement"] = {
      status: "✅ RECOMMENDED",
      description: "Client entity statement for federation-based key management",
      verified: true
    }
  end

  # Check registration type (automatic if client entity statement is provided)
  if StringHelpers.present?(client_entity_statement_path) || StringHelpers.present?(client_entity_statement_url)
    results[:compliance_checks]["Automatic Registration"] = {
      status: "✅ ENABLED",
      description: "Automatic client registration using entity statement",
      verified: true
    }
  end

  results[:all_compliance_verified] = results[:compliance_checks].all? { |_k, v| v[:verified] }

  results
end