legate 0.1.0

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.
Files changed (317) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +345 -0
  4. data/bin/legate +13 -0
  5. data/examples/00_quickstart.rb +51 -0
  6. data/examples/01_simple_agent.rb +105 -0
  7. data/examples/02_multi_tool_agent.rb +140 -0
  8. data/examples/03_custom_tool.rb +93 -0
  9. data/examples/04_agent_instructions.rb +84 -0
  10. data/examples/05_state_and_sessions.rb +91 -0
  11. data/examples/06_callbacks.rb +186 -0
  12. data/examples/07_async_jobs.rb +112 -0
  13. data/examples/08_loop_agent.rb +197 -0
  14. data/examples/09_sequential_workflow.rb +40 -0
  15. data/examples/10_parallel_workflow.rb +34 -0
  16. data/examples/11_agent_delegation.rb +24 -0
  17. data/examples/12_http_client_tool.rb +156 -0
  18. data/examples/13_authentication.rb +220 -0
  19. data/examples/14_mcp_client.rb +154 -0
  20. data/examples/15_mcp_server.rb +79 -0
  21. data/examples/16_webhooks.rb +91 -0
  22. data/examples/README_sequential_agents.md +164 -0
  23. data/examples/advanced/auth/cookie_auth_tool.rb +146 -0
  24. data/examples/advanced/auth/custom_auth_flows_example.rb +626 -0
  25. data/examples/advanced/auth/excon_middleware.rb +317 -0
  26. data/examples/advanced/auth/excon_middleware_auth.rb +399 -0
  27. data/examples/advanced/auth/fiber_auth_example.rb +281 -0
  28. data/examples/advanced/auth/fiber_oidc_example.rb +403 -0
  29. data/examples/advanced/auth/httpbin_bearer_tool.rb +159 -0
  30. data/examples/advanced/auth/oauth2_auth.rb +419 -0
  31. data/examples/advanced/auth/oidc_auth.rb +514 -0
  32. data/examples/advanced/auth/openweather_api.rb +251 -0
  33. data/examples/advanced/auth/openweather_tool.rb +153 -0
  34. data/examples/advanced/auth/query_param_middleware_test.rb +138 -0
  35. data/examples/advanced/auth/service_account.rb +135 -0
  36. data/examples/advanced/auth/test_with_httpbin.rb +202 -0
  37. data/examples/advanced/auth/token_lifecycle_example.rb +428 -0
  38. data/examples/advanced/callback_monitoring.rb +679 -0
  39. data/examples/advanced/mas/fixed_delegation_example.rb +191 -0
  40. data/examples/advanced/mas/loop_workflow.rb +28 -0
  41. data/examples/advanced/mas/mock_planner.rb +77 -0
  42. data/examples/advanced/mas/proper_delegation_example.rb +276 -0
  43. data/examples/advanced/mcp/legate_mcp_server_resource_example.rb +182 -0
  44. data/examples/advanced/mcp/mcp_resource_server_example.rb +309 -0
  45. data/examples/advanced/mcp/mcp_server_async.rb +76 -0
  46. data/examples/advanced/mcp/mcp_server_async_tools.rb +122 -0
  47. data/examples/advanced/mcp/mcp_server_legate_agent.rb +95 -0
  48. data/examples/advanced/mcp/mcp_server_rack.rb +89 -0
  49. data/examples/advanced/random_calculator.rb +104 -0
  50. data/examples/advanced/sleep_agent.rb +153 -0
  51. data/examples/advanced/webhooks/webhook_e2e_runner.rb +110 -0
  52. data/examples/advanced/webhooks/webhook_receiver_agent.rb +58 -0
  53. data/examples/advanced/workflows/task_refinement_loop_agent.rb +278 -0
  54. data/examples/advanced/workflows/travel_planner_auto_sequential.rb +444 -0
  55. data/examples/advanced/workflows/travel_planner_parallel.rb +656 -0
  56. data/examples/advanced/workflows/travel_planner_sequential.rb +512 -0
  57. data/examples/tools/oauth2_example.rb +136 -0
  58. data/examples/tools/sleepy_tool.rb +42 -0
  59. data/lib/legate/activity_log.rb +71 -0
  60. data/lib/legate/agent.rb +959 -0
  61. data/lib/legate/agent_code_generator.rb +185 -0
  62. data/lib/legate/agent_definition.rb +812 -0
  63. data/lib/legate/agentic/decision.rb +49 -0
  64. data/lib/legate/agentic/loop.rb +134 -0
  65. data/lib/legate/agentic.rb +5 -0
  66. data/lib/legate/agents/loop_agent.rb +248 -0
  67. data/lib/legate/agents/parallel_agent.rb +163 -0
  68. data/lib/legate/agents/sequential_agent.rb +190 -0
  69. data/lib/legate/agents.rb +14 -0
  70. data/lib/legate/auth/config.rb +148 -0
  71. data/lib/legate/auth/coordinator.rb +218 -0
  72. data/lib/legate/auth/coordinators/oauth2_coordinator.rb +99 -0
  73. data/lib/legate/auth/coordinators/oidc_coordinator.rb +68 -0
  74. data/lib/legate/auth/coordinators/service_account_coordinator.rb +122 -0
  75. data/lib/legate/auth/credential.rb +157 -0
  76. data/lib/legate/auth/encryption.rb +108 -0
  77. data/lib/legate/auth/error.rb +94 -0
  78. data/lib/legate/auth/exchanged_credential.rb +180 -0
  79. data/lib/legate/auth/excon_middleware.rb +285 -0
  80. data/lib/legate/auth/http_client_utils.rb +364 -0
  81. data/lib/legate/auth/manager.rb +531 -0
  82. data/lib/legate/auth/manager_store.rb +394 -0
  83. data/lib/legate/auth/middleware_factory.rb +290 -0
  84. data/lib/legate/auth/runner.rb +279 -0
  85. data/lib/legate/auth/scheme.rb +125 -0
  86. data/lib/legate/auth/schemes/api_key.rb +212 -0
  87. data/lib/legate/auth/schemes/google_service_account.rb +108 -0
  88. data/lib/legate/auth/schemes/http_bearer.rb +98 -0
  89. data/lib/legate/auth/schemes/oauth2.rb +396 -0
  90. data/lib/legate/auth/schemes/openid_connect.rb +346 -0
  91. data/lib/legate/auth/schemes/service_account.rb +388 -0
  92. data/lib/legate/auth/schemes.rb +40 -0
  93. data/lib/legate/auth/token_manager.rb +362 -0
  94. data/lib/legate/auth/token_store.rb +86 -0
  95. data/lib/legate/auth/tool_context_extension.rb +97 -0
  96. data/lib/legate/auth/tool_integration.rb +188 -0
  97. data/lib/legate/auth/url_guard.rb +81 -0
  98. data/lib/legate/auth.rb +453 -0
  99. data/lib/legate/callbacks/callback_context.rb +71 -0
  100. data/lib/legate/cli/agent_commands.rb +950 -0
  101. data/lib/legate/cli/auth_commands.rb +520 -0
  102. data/lib/legate/cli/base_command.rb +24 -0
  103. data/lib/legate/cli/deployment_commands.rb +934 -0
  104. data/lib/legate/cli/output_helper.rb +108 -0
  105. data/lib/legate/cli/session_commands.rb +138 -0
  106. data/lib/legate/cli/skaffold_commands.rb +223 -0
  107. data/lib/legate/cli/tool_commands.rb +261 -0
  108. data/lib/legate/cli/web_commands.rb +182 -0
  109. data/lib/legate/cli.rb +40 -0
  110. data/lib/legate/configuration/webhooks.rb +113 -0
  111. data/lib/legate/configuration.rb +39 -0
  112. data/lib/legate/definition_store.rb +23 -0
  113. data/lib/legate/errors.rb +118 -0
  114. data/lib/legate/event.rb +161 -0
  115. data/lib/legate/gemini_ai_beta_patch.rb +39 -0
  116. data/lib/legate/generators/agent_generator.rb +412 -0
  117. data/lib/legate/generators/code_validator.rb +48 -0
  118. data/lib/legate/generators/legate/install_generator.rb +35 -0
  119. data/lib/legate/generators/legate/templates/create_legate_tables.rb.tt +36 -0
  120. data/lib/legate/generators/legate/templates/initializer.rb +18 -0
  121. data/lib/legate/generators/runtime_tool_loader.rb +76 -0
  122. data/lib/legate/generators/tool_generator.rb +408 -0
  123. data/lib/legate/generators.rb +11 -0
  124. data/lib/legate/global_definition_registry.rb +506 -0
  125. data/lib/legate/global_tool_manager.rb +135 -0
  126. data/lib/legate/llm/adapter.rb +69 -0
  127. data/lib/legate/llm/gemini.rb +172 -0
  128. data/lib/legate/llm/ollama.rb +80 -0
  129. data/lib/legate/llm.rb +34 -0
  130. data/lib/legate/mcp/client.rb +320 -0
  131. data/lib/legate/mcp/connection/sse.rb +292 -0
  132. data/lib/legate/mcp/connection/stdio.rb +273 -0
  133. data/lib/legate/mcp/connection_manager.rb +103 -0
  134. data/lib/legate/mcp/server/legate_agent_adapter.rb +170 -0
  135. data/lib/legate/mcp/server/legate_direct_agent_adapter.rb +140 -0
  136. data/lib/legate/mcp/server/legate_tool_adapter.rb +119 -0
  137. data/lib/legate/mcp/tool_wrapper.rb +138 -0
  138. data/lib/legate/mcp/util/schema_converter.rb +134 -0
  139. data/lib/legate/mcp.rb +23 -0
  140. data/lib/legate/plan_executor.rb +375 -0
  141. data/lib/legate/planner.rb +839 -0
  142. data/lib/legate/rails/railtie.rb +43 -0
  143. data/lib/legate/rails.rb +9 -0
  144. data/lib/legate/redaction.rb +32 -0
  145. data/lib/legate/session.rb +299 -0
  146. data/lib/legate/session_service/active_record.rb +300 -0
  147. data/lib/legate/session_service/base.rb +68 -0
  148. data/lib/legate/session_service/event_broadcast.rb +74 -0
  149. data/lib/legate/session_service/in_memory.rb +188 -0
  150. data/lib/legate/tool/metadata_dsl.rb +122 -0
  151. data/lib/legate/tool.rb +276 -0
  152. data/lib/legate/tool_code_generator.rb +103 -0
  153. data/lib/legate/tool_context.rb +350 -0
  154. data/lib/legate/tool_loader.rb +39 -0
  155. data/lib/legate/tool_registry.rb +73 -0
  156. data/lib/legate/tool_result.rb +61 -0
  157. data/lib/legate/tools/agent_tool.rb +187 -0
  158. data/lib/legate/tools/base/http_client.rb +319 -0
  159. data/lib/legate/tools/base/safe_url.rb +56 -0
  160. data/lib/legate/tools/base_async_job_tool.rb +91 -0
  161. data/lib/legate/tools/calculator.rb +89 -0
  162. data/lib/legate/tools/cat_facts.rb +81 -0
  163. data/lib/legate/tools/check_job_status_tool.rb +48 -0
  164. data/lib/legate/tools/current_time_tool.rb +64 -0
  165. data/lib/legate/tools/echo.rb +43 -0
  166. data/lib/legate/tools/http_request_tool.rb +105 -0
  167. data/lib/legate/tools/random_number_tool.rb +64 -0
  168. data/lib/legate/tools/read_webpage_tool.rb +92 -0
  169. data/lib/legate/tools/sleepy_tool.rb +74 -0
  170. data/lib/legate/tools/webhook_tool.rb +146 -0
  171. data/lib/legate/version.rb +5 -0
  172. data/lib/legate/web/app.rb +984 -0
  173. data/lib/legate/web/public/css/main.css +4980 -0
  174. data/lib/legate/web/public/images/favicon-256.png +0 -0
  175. data/lib/legate/web/public/images/favicon-32.png +0 -0
  176. data/lib/legate/web/public/images/legate-logo-dark.png +0 -0
  177. data/lib/legate/web/public/images/legate-logo-light.png +0 -0
  178. data/lib/legate/web/public/js/legate.js +616 -0
  179. data/lib/legate/web/public/styles/main.scss +4402 -0
  180. data/lib/legate/web/routes/agent_authentication_routes.rb +530 -0
  181. data/lib/legate/web/routes/agent_definition_routes.rb +803 -0
  182. data/lib/legate/web/routes/agent_generator_routes.rb +80 -0
  183. data/lib/legate/web/routes/agent_interaction_routes.rb +734 -0
  184. data/lib/legate/web/routes/agent_runtime_routes.rb +323 -0
  185. data/lib/legate/web/routes/api_routes.rb +56 -0
  186. data/lib/legate/web/routes/authentication_routes.rb +1541 -0
  187. data/lib/legate/web/routes/core_routes.rb +111 -0
  188. data/lib/legate/web/routes/documentation_routes.rb +220 -0
  189. data/lib/legate/web/routes/tool_generator_routes.rb +81 -0
  190. data/lib/legate/web/routes/tools_ui_routes.rb +207 -0
  191. data/lib/legate/web/sass_compiler.rb +73 -0
  192. data/lib/legate/web/views/_active_session_info.slim +25 -0
  193. data/lib/legate/web/views/_activity_list.slim +55 -0
  194. data/lib/legate/web/views/_agent_card.slim +56 -0
  195. data/lib/legate/web/views/_agent_generator_modal.slim +382 -0
  196. data/lib/legate/web/views/_agent_status_controls.slim +71 -0
  197. data/lib/legate/web/views/_agent_tool_table.slim +74 -0
  198. data/lib/legate/web/views/_chat_message.slim +95 -0
  199. data/lib/legate/web/views/_display_agent_configuration.slim +26 -0
  200. data/lib/legate/web/views/_display_agent_description.slim +11 -0
  201. data/lib/legate/web/views/_display_agent_fallback.slim +15 -0
  202. data/lib/legate/web/views/_display_agent_hierarchy.slim +93 -0
  203. data/lib/legate/web/views/_display_agent_instruction.slim +17 -0
  204. data/lib/legate/web/views/_display_agent_mcp.slim +13 -0
  205. data/lib/legate/web/views/_display_agent_model.slim +17 -0
  206. data/lib/legate/web/views/_display_agent_name.slim +42 -0
  207. data/lib/legate/web/views/_display_agent_output_key.slim +26 -0
  208. data/lib/legate/web/views/_display_agent_type.slim +65 -0
  209. data/lib/legate/web/views/_edit_agent_configuration.slim +74 -0
  210. data/lib/legate/web/views/_edit_agent_description.slim +16 -0
  211. data/lib/legate/web/views/_edit_agent_fallback.slim +25 -0
  212. data/lib/legate/web/views/_edit_agent_hierarchy.slim +98 -0
  213. data/lib/legate/web/views/_edit_agent_instruction.slim +49 -0
  214. data/lib/legate/web/views/_edit_agent_mcp.slim +33 -0
  215. data/lib/legate/web/views/_edit_agent_model.slim +23 -0
  216. data/lib/legate/web/views/_edit_agent_output_key.slim +36 -0
  217. data/lib/legate/web/views/_edit_agent_tools.slim +40 -0
  218. data/lib/legate/web/views/_edit_agent_type.slim +67 -0
  219. data/lib/legate/web/views/_session_error.slim +4 -0
  220. data/lib/legate/web/views/_skeleton.slim +69 -0
  221. data/lib/legate/web/views/_tool_card.slim +9 -0
  222. data/lib/legate/web/views/_tool_generator_modal.slim +311 -0
  223. data/lib/legate/web/views/agent.slim +436 -0
  224. data/lib/legate/web/views/agent_auth.slim +562 -0
  225. data/lib/legate/web/views/agents.slim +369 -0
  226. data/lib/legate/web/views/auth.slim +112 -0
  227. data/lib/legate/web/views/auth_credential_detail.slim +327 -0
  228. data/lib/legate/web/views/auth_credentials.slim +261 -0
  229. data/lib/legate/web/views/auth_debug.slim +94 -0
  230. data/lib/legate/web/views/auth_mapping_detail.slim +151 -0
  231. data/lib/legate/web/views/auth_mapping_new.slim +123 -0
  232. data/lib/legate/web/views/auth_mappings.slim +120 -0
  233. data/lib/legate/web/views/auth_scheme_detail.slim +274 -0
  234. data/lib/legate/web/views/auth_schemes.slim +259 -0
  235. data/lib/legate/web/views/auth_test.slim +418 -0
  236. data/lib/legate/web/views/chat.slim +192 -0
  237. data/lib/legate/web/views/docs_index.slim +105 -0
  238. data/lib/legate/web/views/docs_show.slim +105 -0
  239. data/lib/legate/web/views/error_404.slim +5 -0
  240. data/lib/legate/web/views/index.slim +148 -0
  241. data/lib/legate/web/views/layout.slim +144 -0
  242. data/lib/legate/web/views/tool_detail.slim +87 -0
  243. data/lib/legate/web/views/tools.slim +50 -0
  244. data/lib/legate/web/webhook_listener.rb +367 -0
  245. data/lib/legate/web.rb +9 -0
  246. data/lib/legate.rb +220 -0
  247. data/public/docs/advanced/callbacks.md +828 -0
  248. data/public/docs/advanced/mcp_schema_conversion.md +59 -0
  249. data/public/docs/authentication/api_reference/config.md +210 -0
  250. data/public/docs/authentication/api_reference/credential.md +246 -0
  251. data/public/docs/authentication/api_reference/encryption.md +218 -0
  252. data/public/docs/authentication/api_reference/exchanged_credential.md +271 -0
  253. data/public/docs/authentication/api_reference/excon_middleware.md +175 -0
  254. data/public/docs/authentication/api_reference/index.md +30 -0
  255. data/public/docs/authentication/api_reference/scheme.md +250 -0
  256. data/public/docs/authentication/api_reference/schemes/api_key.md +175 -0
  257. data/public/docs/authentication/api_reference/schemes/google_service_account.md +221 -0
  258. data/public/docs/authentication/api_reference/schemes/http_bearer.md +169 -0
  259. data/public/docs/authentication/api_reference/schemes/oauth2.md +343 -0
  260. data/public/docs/authentication/api_reference/schemes/oidc.md +73 -0
  261. data/public/docs/authentication/api_reference/schemes/openid_connect.md +311 -0
  262. data/public/docs/authentication/api_reference/schemes/service_account.md +287 -0
  263. data/public/docs/authentication/api_reference/token_manager.md +221 -0
  264. data/public/docs/authentication/api_reference/token_store.md +146 -0
  265. data/public/docs/authentication/api_reference/tool_context_extension.md +166 -0
  266. data/public/docs/authentication/guides/api_key.md +190 -0
  267. data/public/docs/authentication/guides/bearer.md +172 -0
  268. data/public/docs/authentication/guides/configuration.md +255 -0
  269. data/public/docs/authentication/guides/custom_flow.md +523 -0
  270. data/public/docs/authentication/guides/index.md +24 -0
  271. data/public/docs/authentication/guides/migration.md +435 -0
  272. data/public/docs/authentication/guides/oauth2.md +252 -0
  273. data/public/docs/authentication/guides/oidc.md +241 -0
  274. data/public/docs/authentication/guides/overview.md +155 -0
  275. data/public/docs/authentication/guides/secure_storage.md +301 -0
  276. data/public/docs/authentication/guides/service_account.md +228 -0
  277. data/public/docs/authentication/guides/token_lifecycle.md +295 -0
  278. data/public/docs/authentication/guides/web_ui_integration.md +504 -0
  279. data/public/docs/authentication/index.md +58 -0
  280. data/public/docs/authentication/troubleshooting/credential_storage.md +550 -0
  281. data/public/docs/authentication/troubleshooting/environment_variables.md +540 -0
  282. data/public/docs/authentication/troubleshooting/index.md +11 -0
  283. data/public/docs/authentication/troubleshooting/oauth2_issues.md +220 -0
  284. data/public/docs/authentication/troubleshooting/oidc_issues.md +412 -0
  285. data/public/docs/authentication/troubleshooting/token_refresh.md +338 -0
  286. data/public/docs/cli/legate_cli_usage.md +363 -0
  287. data/public/docs/core_concepts/legate_agent_lifecycle.md +124 -0
  288. data/public/docs/core_concepts/legate_architecture_overview.md +110 -0
  289. data/public/docs/core_concepts/legate_configuration.md +116 -0
  290. data/public/docs/core_concepts/legate_definition_store.md +102 -0
  291. data/public/docs/core_concepts/legate_planner.md +94 -0
  292. data/public/docs/core_concepts/legate_session_service.md +104 -0
  293. data/public/docs/error_handling/legate_error_handling.md +122 -0
  294. data/public/docs/examples.md +199 -0
  295. data/public/docs/getting_started.md +111 -0
  296. data/public/docs/guides/agentic_agents.md +137 -0
  297. data/public/docs/guides/ai_code_generators.md +437 -0
  298. data/public/docs/guides/auto_loading.md +326 -0
  299. data/public/docs/guides/configuring_agent_webhooks.md +219 -0
  300. data/public/docs/guides/http_client_usage.md +264 -0
  301. data/public/docs/guides/llm_providers.md +137 -0
  302. data/public/docs/guides/mcp_client_integration.md +232 -0
  303. data/public/docs/guides/mcp_server_exposure.md +206 -0
  304. data/public/docs/guides/rails_integration.md +128 -0
  305. data/public/docs/guides/sending_outbound_webhooks.md +227 -0
  306. data/public/docs/guides/streaming.md +112 -0
  307. data/public/docs/guides/webhooks.md +288 -0
  308. data/public/docs/introduction.md +51 -0
  309. data/public/docs/multi_agent_systems/advanced_features.md +57 -0
  310. data/public/docs/multi_agent_systems/agent_delegation.md +190 -0
  311. data/public/docs/multi_agent_systems/agent_hierarchy.md +49 -0
  312. data/public/docs/multi_agent_systems/state_management.md +47 -0
  313. data/public/docs/multi_agent_systems/workflow_agents.md +72 -0
  314. data/public/docs/tools/legate_built_in_tools.md +332 -0
  315. data/public/docs/tools/legate_tools_and_registry.md +263 -0
  316. data/public/docs/web_ui/legate_web_ui.md +137 -0
  317. metadata +823 -0
@@ -0,0 +1,346 @@
1
+ # File: lib/legate/auth/schemes/openid_connect.rb
2
+ # frozen_string_literal: true
3
+
4
+ require 'oauth2'
5
+ require 'securerandom'
6
+ require 'net/http'
7
+ require 'json'
8
+ require 'jwt'
9
+ require_relative 'oauth2'
10
+
11
+ module Legate
12
+ module Auth
13
+ module Schemes
14
+ # Implements OpenID Connect authentication
15
+ # Extends OAuth2 with OpenID Connect specific features
16
+ class OpenIDConnect < OAuth2
17
+ # @return [String, nil] The URL for the OpenID Connect discovery document
18
+ attr_reader :discovery_url
19
+
20
+ # @return [String, nil] The URL for the JWK Set
21
+ attr_reader :jwks_url
22
+
23
+ # @return [String, nil] The userinfo endpoint URL
24
+ attr_reader :userinfo_url
25
+
26
+ # @return [String, nil] The issuer identifier
27
+ attr_reader :issuer
28
+
29
+ # @return [String, nil] The provider URI
30
+ attr_reader :provider_uri
31
+
32
+ # @return [String] The client ID
33
+ attr_reader :client_id
34
+
35
+ # Initialize a new OpenID Connect scheme
36
+ # @param authorization_url [String, nil] The authorization URL
37
+ # @param token_url [String, nil] The token URL
38
+ # @param discovery_url [String, nil] The URL for the discovery document (optional if endpoints provided)
39
+ # @param jwks_url [String, nil] The URL for the JWKS document (optional if discovery URL provided)
40
+ # @param userinfo_url [String, nil] The URL for the userinfo endpoint (optional)
41
+ # @param scopes [Array<String>, String, nil] The requested scopes
42
+ # @param use_pkce [Boolean] Whether to use PKCE
43
+ # @param additional_params [Hash, nil] Additional parameters for authorization requests
44
+ # @param revocation_url [String, nil] The URL for the revocation endpoint
45
+ # @param client_id [String, nil] The client ID
46
+ # @param client_secret [String, nil] The client secret
47
+ # @param redirect_uri [String, nil] The redirect URI
48
+ # @param kwargs [Hash] Additional options to pass to the OAuth2 parent class
49
+ # @param config [Hash] A config hash containing all options (alternative to individual parameters)
50
+ def initialize(first_arg = nil, authorization_url: nil, token_url: nil, discovery_url: nil,
51
+ jwks_url: nil, userinfo_url: nil, scopes: nil, use_pkce: true,
52
+ additional_params: nil, revocation_url: nil, client_id: nil,
53
+ client_secret: nil, redirect_uri: nil, **kwargs)
54
+ # Handle direct hash configuration in first_arg or config param
55
+ config = first_arg if first_arg.is_a?(Hash)
56
+
57
+ if config.is_a?(Hash)
58
+ # Extract OpenID Connect specific properties from config
59
+ @discovery_url = config[:discovery_url] || config[:provider_uri] && "#{config[:provider_uri]}/.well-known/openid-configuration"
60
+ @jwks_url = config[:jwks_url]
61
+ @userinfo_url = config[:userinfo_url]
62
+ @client_id = config[:client_id]
63
+ @client_secret = config[:client_secret]
64
+ @redirect_uri = config[:redirect_uri]
65
+ @provider_uri = config[:provider_uri]
66
+ @issuer = config[:issuer]
67
+ authorization_url = config[:authorization_url] || config[:authorization_endpoint]
68
+ token_url = config[:token_url] || config[:token_endpoint]
69
+ scopes = config[:scopes] || config[:scope]
70
+ use_pkce = config.key?(:use_pkce) ? config[:use_pkce] : true
71
+ additional_params = config[:additional_params]
72
+ revocation_url = config[:revocation_url]
73
+
74
+ # Move any remaining options to kwargs
75
+ extra_opts = config.reject { |k, _|
76
+ %i[discovery_url jwks_url userinfo_url client_id
77
+ client_secret redirect_uri provider_uri issuer authorization_url
78
+ authorization_endpoint token_url token_endpoint scopes scope
79
+ use_pkce additional_params revocation_url].include?(k)
80
+ }
81
+ kwargs = kwargs.merge(extra_opts)
82
+ else
83
+ # Store OpenID Connect specific properties from parameters
84
+ @discovery_url = discovery_url
85
+ @jwks_url = jwks_url
86
+ @userinfo_url = userinfo_url
87
+ @client_id = client_id
88
+ @client_secret = client_secret
89
+ @redirect_uri = redirect_uri
90
+ @provider_uri = kwargs[:provider_uri]
91
+ @issuer = kwargs[:issuer]
92
+ end
93
+
94
+ # If discovery URL is provided, try to fetch endpoints
95
+ if @discovery_url && (authorization_url.nil? || token_url.nil? || @userinfo_url.nil?)
96
+ endpoints = discover_endpoints
97
+ authorization_url ||= endpoints[:authorization_endpoint]
98
+ token_url ||= endpoints[:token_endpoint]
99
+ @jwks_url ||= endpoints[:jwks_uri]
100
+ @userinfo_url ||= endpoints[:userinfo_endpoint]
101
+ @issuer ||= endpoints[:issuer]
102
+ end
103
+
104
+ # Parse and add the openid scope if not present
105
+ oidc_scopes = parse_scopes(scopes)
106
+ oidc_scopes << 'openid' unless oidc_scopes.include?('openid')
107
+
108
+ # Call the parent constructor with merged settings
109
+ super(
110
+ authorization_url: authorization_url,
111
+ token_url: token_url,
112
+ scopes: oidc_scopes,
113
+ use_pkce: use_pkce,
114
+ additional_params: additional_params,
115
+ revocation_url: revocation_url,
116
+ client_id: @client_id,
117
+ client_secret: @client_secret,
118
+ redirect_uri: @redirect_uri,
119
+ **kwargs
120
+ )
121
+
122
+ # Make sure client_id is properly set after parent initialization
123
+ @client_id = kwargs[:client_id] if @client_id.nil?
124
+
125
+ # Validate required fields if this is a direct instance (not a subclass)
126
+ validate! if self.class == Legate::Auth::Schemes::OpenIDConnect
127
+ end
128
+
129
+ # @return [Symbol] The scheme type
130
+ def scheme_type
131
+ :openid_connect
132
+ end
133
+
134
+ # Validates the scheme configuration
135
+ # @raise [Legate::Auth::SchemeValidationError] If the configuration is invalid
136
+ def validate!
137
+ # Only skip validation in test environment if FORCE_VALIDATE is not true
138
+ in_test = ENV['RSPEC_ENV'] == 'test'
139
+ force_validate = ENV['FORCE_VALIDATE'] == 'true'
140
+
141
+ return if in_test && !force_validate
142
+
143
+ raise Legate::Auth::SchemeValidationError, 'Authorization URL is required' if authorization_url.nil? || authorization_url.to_s.strip.empty?
144
+
145
+ return unless token_url.nil? || token_url.to_s.strip.empty?
146
+
147
+ raise Legate::Auth::SchemeValidationError, 'Token URL is required'
148
+ end
149
+
150
+ # Override to prevent the base URL from being modified with default query parameters
151
+ # Just return the base authorization_url without query parameters
152
+ attr_reader :authorization_url
153
+
154
+ # Build the authorization URI for the OpenID Connect flow
155
+ # @param config [Legate::Auth::Config] The authentication configuration
156
+ # @param redirect_uri [String, nil] The redirect URI for the authorization request
157
+ # @param state [String, nil] A state parameter for CSRF protection
158
+ # @return [Hash] The authorization URI and any additional parameters
159
+ def build_authorization_uri(config, redirect_uri = nil, state = nil)
160
+ # Generate nonce for OpenID Connect
161
+ nonce = config.options[:nonce] || SecureRandom.hex(16)
162
+
163
+ # Store nonce in config for later verification
164
+ config.options[:nonce] = nonce
165
+
166
+ # Add nonce to parameters
167
+ additional_params = @additional_params ? @additional_params.dup : {}
168
+ additional_params['nonce'] = nonce
169
+
170
+ # Ensure 'openid' scope is included
171
+ oidc_scopes = @scopes.dup
172
+ oidc_scopes << 'openid' unless oidc_scopes.include?('openid')
173
+
174
+ # Temporarily store modified scopes
175
+ original_scopes = @scopes
176
+ @scopes = oidc_scopes
177
+
178
+ # Temporarily modify additional_params
179
+ original_additional_params = @additional_params
180
+ @additional_params = additional_params
181
+
182
+ # Call the parent method
183
+ result = super(config, redirect_uri, state)
184
+
185
+ # Restore original additional_params and scopes
186
+ @additional_params = original_additional_params
187
+ @scopes = original_scopes
188
+
189
+ result
190
+ end
191
+
192
+ # Override exchange_token to set correct auth_type
193
+ # @param config [Legate::Auth::Config] The authentication configuration
194
+ # @param credential [Legate::Auth::Credential] The credential with client information
195
+ # @return [Legate::Auth::ExchangedCredential] The exchanged credential
196
+ def exchange_token(config, credential)
197
+ result = super(config, credential)
198
+
199
+ # Modify the auth_type to be :openid_connect if successful
200
+ result.instance_variable_set(:@auth_type, :openid_connect) if result && result.is_a?(Legate::Auth::ExchangedCredential)
201
+
202
+ result
203
+ end
204
+
205
+ # Convert to a hash representation
206
+ # @return [Hash] The hash representation of the scheme
207
+ def to_h
208
+ hash = super
209
+ hash[:discovery_url] = @discovery_url if @discovery_url
210
+ hash[:jwks_url] = @jwks_url if @jwks_url
211
+ hash[:userinfo_url] = @userinfo_url if @userinfo_url
212
+ hash
213
+ end
214
+
215
+ # Discover OpenID Connect endpoints from the discovery URL
216
+ # @return [Hash] The discovered endpoints
217
+ def discover_endpoints
218
+ return {} unless @discovery_url
219
+
220
+ # Skip discovery in test environment to avoid HTTP calls
221
+ return {} if ENV['RSPEC_ENV'] == 'test'
222
+
223
+ begin
224
+ validate_auth_url!(@discovery_url, label: 'Discovery URL')
225
+ uri = URI(@discovery_url)
226
+ response = Net::HTTP.get_response(uri)
227
+
228
+ unless response.is_a?(Net::HTTPSuccess)
229
+ Legate.logger.error("Failed to fetch OpenID Connect discovery document: #{response.code} #{response.message}")
230
+ return {}
231
+ end
232
+
233
+ discovery_data = JSON.parse(response.body)
234
+
235
+ {
236
+ authorization_endpoint: discovery_data['authorization_endpoint'],
237
+ token_endpoint: discovery_data['token_endpoint'],
238
+ jwks_uri: discovery_data['jwks_uri'],
239
+ userinfo_endpoint: discovery_data['userinfo_endpoint'],
240
+ issuer: discovery_data['issuer']
241
+ }
242
+ rescue StandardError => e
243
+ Legate.logger.error("Error discovering OpenID Connect endpoints: #{e.message}")
244
+ {}
245
+ end
246
+ end
247
+
248
+ # Retrieve user information using the access token
249
+ # @param access_token [String] The access token
250
+ # @return [Hash] The user information
251
+ # @raise [Legate::Auth::Errors::AuthenticationError] If user info could not be retrieved
252
+ def get_userinfo(access_token)
253
+ # Use the configured userinfo endpoint, fall back to issuer-based URL
254
+ endpoint = @userinfo_url
255
+ endpoint = "#{@issuer}/userinfo" if endpoint.nil? && @issuer
256
+
257
+ raise Legate::Auth::Errors::AuthenticationError, 'Userinfo endpoint not configured' unless endpoint
258
+
259
+ begin
260
+ validate_auth_url!(endpoint, label: 'Userinfo URL')
261
+ response = Faraday.get(endpoint) do |req|
262
+ req.headers['Authorization'] = "Bearer #{access_token}"
263
+ end
264
+
265
+ raise Legate::Auth::Errors::AuthenticationError, "Failed to fetch userinfo: #{response.status} #{response.reason_phrase}" unless response.status == 200
266
+
267
+ JSON.parse(response.body)
268
+ rescue Faraday::Error => e
269
+ raise Legate::Auth::Errors::AuthenticationError, "Error fetching userinfo: #{e.message}"
270
+ rescue JSON::ParserError => e
271
+ raise Legate::Auth::Errors::AuthenticationError, "Invalid userinfo response: #{e.message}"
272
+ rescue StandardError => e
273
+ raise Legate::Auth::Errors::AuthenticationError, "Unexpected error fetching userinfo: #{e.message}"
274
+ end
275
+ end
276
+
277
+ # Verify an ID token using the provider's JWKS for signature verification.
278
+ # @param id_token [String] The ID token to verify
279
+ # @param nonce [String, nil] The nonce to validate against
280
+ # @param audience [String, nil] The expected audience
281
+ # @return [Hash] The verified ID token claims
282
+ # @raise [Legate::Auth::TokenVerificationError] If token verification fails
283
+ def verify_id_token(id_token, nonce = nil, audience = nil)
284
+ jwks = fetch_jwks
285
+ algorithms = %w[RS256 RS384 RS512 ES256 ES384 ES512]
286
+
287
+ decode_opts = { algorithms: algorithms }
288
+ decode_opts[:iss] = @issuer if @issuer
289
+ decode_opts[:verify_iss] = true if @issuer
290
+ decode_opts[:aud] = audience if audience
291
+ decode_opts[:verify_aud] = true if audience
292
+
293
+ if jwks && !jwks.empty?
294
+ jwk_set = JWT::JWK::Set.new(jwks)
295
+ payload, _header = JWT.decode(id_token, nil, true, decode_opts) do |header|
296
+ jwk_set.find { |key| key[:kid] == header['kid'] }&.public_key
297
+ end
298
+ else
299
+ # No JWKS available — decode without signature verification
300
+ # but still validate claims. Log a warning since this weakens security.
301
+ Legate.logger.warn('OpenIDConnect: No JWKS available for signature verification — decoding without signature check')
302
+ payload, _header = JWT.decode(id_token, nil, false, algorithms: algorithms)
303
+ end
304
+
305
+ raise Legate::Auth::TokenVerificationError, 'ID token nonce mismatch' if nonce && payload['nonce'] != nonce
306
+
307
+ payload
308
+ rescue JWT::DecodeError => e
309
+ raise Legate::Auth::TokenVerificationError, "Failed to verify ID token: #{e.message}"
310
+ rescue StandardError => e
311
+ raise Legate::Auth::TokenVerificationError, "ID token verification failed: #{e.message}"
312
+ end
313
+
314
+ private
315
+
316
+ # Fetch the provider's JWKS, with a 5-minute TTL cache.
317
+ # @return [Hash, nil] The parsed JWKS document or nil if unavailable
318
+ def fetch_jwks
319
+ now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
320
+ return @jwks_cache if @jwks_cache && @jwks_cache_expires_at && now < @jwks_cache_expires_at
321
+
322
+ jwks_endpoint = @jwks_url
323
+ return nil unless jwks_endpoint
324
+
325
+ validate_auth_url!(jwks_endpoint, label: 'JWKS URL')
326
+ uri = URI(jwks_endpoint)
327
+ response = Net::HTTP.get_response(uri)
328
+ unless response.is_a?(Net::HTTPSuccess)
329
+ Legate.logger.error("OpenIDConnect: Failed to fetch JWKS: #{response.code}")
330
+ return nil
331
+ end
332
+
333
+ @jwks_cache = JSON.parse(response.body)
334
+ @jwks_cache_expires_at = now + 300 # 5-minute TTL
335
+ @jwks_cache
336
+ rescue StandardError => e
337
+ Legate.logger.error("OpenIDConnect: Error fetching JWKS: #{e.message}")
338
+ nil
339
+ end
340
+ end
341
+
342
+ # Alias for backward compatibility
343
+ OIDC = OpenIDConnect
344
+ end
345
+ end
346
+ end