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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +345 -0
- data/bin/legate +13 -0
- data/examples/00_quickstart.rb +51 -0
- data/examples/01_simple_agent.rb +105 -0
- data/examples/02_multi_tool_agent.rb +140 -0
- data/examples/03_custom_tool.rb +93 -0
- data/examples/04_agent_instructions.rb +84 -0
- data/examples/05_state_and_sessions.rb +91 -0
- data/examples/06_callbacks.rb +186 -0
- data/examples/07_async_jobs.rb +112 -0
- data/examples/08_loop_agent.rb +197 -0
- data/examples/09_sequential_workflow.rb +40 -0
- data/examples/10_parallel_workflow.rb +34 -0
- data/examples/11_agent_delegation.rb +24 -0
- data/examples/12_http_client_tool.rb +156 -0
- data/examples/13_authentication.rb +220 -0
- data/examples/14_mcp_client.rb +154 -0
- data/examples/15_mcp_server.rb +79 -0
- data/examples/16_webhooks.rb +91 -0
- data/examples/README_sequential_agents.md +164 -0
- data/examples/advanced/auth/cookie_auth_tool.rb +146 -0
- data/examples/advanced/auth/custom_auth_flows_example.rb +626 -0
- data/examples/advanced/auth/excon_middleware.rb +317 -0
- data/examples/advanced/auth/excon_middleware_auth.rb +399 -0
- data/examples/advanced/auth/fiber_auth_example.rb +281 -0
- data/examples/advanced/auth/fiber_oidc_example.rb +403 -0
- data/examples/advanced/auth/httpbin_bearer_tool.rb +159 -0
- data/examples/advanced/auth/oauth2_auth.rb +419 -0
- data/examples/advanced/auth/oidc_auth.rb +514 -0
- data/examples/advanced/auth/openweather_api.rb +251 -0
- data/examples/advanced/auth/openweather_tool.rb +153 -0
- data/examples/advanced/auth/query_param_middleware_test.rb +138 -0
- data/examples/advanced/auth/service_account.rb +135 -0
- data/examples/advanced/auth/test_with_httpbin.rb +202 -0
- data/examples/advanced/auth/token_lifecycle_example.rb +428 -0
- data/examples/advanced/callback_monitoring.rb +679 -0
- data/examples/advanced/mas/fixed_delegation_example.rb +191 -0
- data/examples/advanced/mas/loop_workflow.rb +28 -0
- data/examples/advanced/mas/mock_planner.rb +77 -0
- data/examples/advanced/mas/proper_delegation_example.rb +276 -0
- data/examples/advanced/mcp/legate_mcp_server_resource_example.rb +182 -0
- data/examples/advanced/mcp/mcp_resource_server_example.rb +309 -0
- data/examples/advanced/mcp/mcp_server_async.rb +76 -0
- data/examples/advanced/mcp/mcp_server_async_tools.rb +122 -0
- data/examples/advanced/mcp/mcp_server_legate_agent.rb +95 -0
- data/examples/advanced/mcp/mcp_server_rack.rb +89 -0
- data/examples/advanced/random_calculator.rb +104 -0
- data/examples/advanced/sleep_agent.rb +153 -0
- data/examples/advanced/webhooks/webhook_e2e_runner.rb +110 -0
- data/examples/advanced/webhooks/webhook_receiver_agent.rb +58 -0
- data/examples/advanced/workflows/task_refinement_loop_agent.rb +278 -0
- data/examples/advanced/workflows/travel_planner_auto_sequential.rb +444 -0
- data/examples/advanced/workflows/travel_planner_parallel.rb +656 -0
- data/examples/advanced/workflows/travel_planner_sequential.rb +512 -0
- data/examples/tools/oauth2_example.rb +136 -0
- data/examples/tools/sleepy_tool.rb +42 -0
- data/lib/legate/activity_log.rb +71 -0
- data/lib/legate/agent.rb +959 -0
- data/lib/legate/agent_code_generator.rb +185 -0
- data/lib/legate/agent_definition.rb +812 -0
- data/lib/legate/agentic/decision.rb +49 -0
- data/lib/legate/agentic/loop.rb +134 -0
- data/lib/legate/agentic.rb +5 -0
- data/lib/legate/agents/loop_agent.rb +248 -0
- data/lib/legate/agents/parallel_agent.rb +163 -0
- data/lib/legate/agents/sequential_agent.rb +190 -0
- data/lib/legate/agents.rb +14 -0
- data/lib/legate/auth/config.rb +148 -0
- data/lib/legate/auth/coordinator.rb +218 -0
- data/lib/legate/auth/coordinators/oauth2_coordinator.rb +99 -0
- data/lib/legate/auth/coordinators/oidc_coordinator.rb +68 -0
- data/lib/legate/auth/coordinators/service_account_coordinator.rb +122 -0
- data/lib/legate/auth/credential.rb +157 -0
- data/lib/legate/auth/encryption.rb +108 -0
- data/lib/legate/auth/error.rb +94 -0
- data/lib/legate/auth/exchanged_credential.rb +180 -0
- data/lib/legate/auth/excon_middleware.rb +285 -0
- data/lib/legate/auth/http_client_utils.rb +364 -0
- data/lib/legate/auth/manager.rb +531 -0
- data/lib/legate/auth/manager_store.rb +394 -0
- data/lib/legate/auth/middleware_factory.rb +290 -0
- data/lib/legate/auth/runner.rb +279 -0
- data/lib/legate/auth/scheme.rb +125 -0
- data/lib/legate/auth/schemes/api_key.rb +212 -0
- data/lib/legate/auth/schemes/google_service_account.rb +108 -0
- data/lib/legate/auth/schemes/http_bearer.rb +98 -0
- data/lib/legate/auth/schemes/oauth2.rb +396 -0
- data/lib/legate/auth/schemes/openid_connect.rb +346 -0
- data/lib/legate/auth/schemes/service_account.rb +388 -0
- data/lib/legate/auth/schemes.rb +40 -0
- data/lib/legate/auth/token_manager.rb +362 -0
- data/lib/legate/auth/token_store.rb +86 -0
- data/lib/legate/auth/tool_context_extension.rb +97 -0
- data/lib/legate/auth/tool_integration.rb +188 -0
- data/lib/legate/auth/url_guard.rb +81 -0
- data/lib/legate/auth.rb +453 -0
- data/lib/legate/callbacks/callback_context.rb +71 -0
- data/lib/legate/cli/agent_commands.rb +950 -0
- data/lib/legate/cli/auth_commands.rb +520 -0
- data/lib/legate/cli/base_command.rb +24 -0
- data/lib/legate/cli/deployment_commands.rb +934 -0
- data/lib/legate/cli/output_helper.rb +108 -0
- data/lib/legate/cli/session_commands.rb +138 -0
- data/lib/legate/cli/skaffold_commands.rb +223 -0
- data/lib/legate/cli/tool_commands.rb +261 -0
- data/lib/legate/cli/web_commands.rb +182 -0
- data/lib/legate/cli.rb +40 -0
- data/lib/legate/configuration/webhooks.rb +113 -0
- data/lib/legate/configuration.rb +39 -0
- data/lib/legate/definition_store.rb +23 -0
- data/lib/legate/errors.rb +118 -0
- data/lib/legate/event.rb +161 -0
- data/lib/legate/gemini_ai_beta_patch.rb +39 -0
- data/lib/legate/generators/agent_generator.rb +412 -0
- data/lib/legate/generators/code_validator.rb +48 -0
- data/lib/legate/generators/legate/install_generator.rb +35 -0
- data/lib/legate/generators/legate/templates/create_legate_tables.rb.tt +36 -0
- data/lib/legate/generators/legate/templates/initializer.rb +18 -0
- data/lib/legate/generators/runtime_tool_loader.rb +76 -0
- data/lib/legate/generators/tool_generator.rb +408 -0
- data/lib/legate/generators.rb +11 -0
- data/lib/legate/global_definition_registry.rb +506 -0
- data/lib/legate/global_tool_manager.rb +135 -0
- data/lib/legate/llm/adapter.rb +69 -0
- data/lib/legate/llm/gemini.rb +172 -0
- data/lib/legate/llm/ollama.rb +80 -0
- data/lib/legate/llm.rb +34 -0
- data/lib/legate/mcp/client.rb +320 -0
- data/lib/legate/mcp/connection/sse.rb +292 -0
- data/lib/legate/mcp/connection/stdio.rb +273 -0
- data/lib/legate/mcp/connection_manager.rb +103 -0
- data/lib/legate/mcp/server/legate_agent_adapter.rb +170 -0
- data/lib/legate/mcp/server/legate_direct_agent_adapter.rb +140 -0
- data/lib/legate/mcp/server/legate_tool_adapter.rb +119 -0
- data/lib/legate/mcp/tool_wrapper.rb +138 -0
- data/lib/legate/mcp/util/schema_converter.rb +134 -0
- data/lib/legate/mcp.rb +23 -0
- data/lib/legate/plan_executor.rb +375 -0
- data/lib/legate/planner.rb +839 -0
- data/lib/legate/rails/railtie.rb +43 -0
- data/lib/legate/rails.rb +9 -0
- data/lib/legate/redaction.rb +32 -0
- data/lib/legate/session.rb +299 -0
- data/lib/legate/session_service/active_record.rb +300 -0
- data/lib/legate/session_service/base.rb +68 -0
- data/lib/legate/session_service/event_broadcast.rb +74 -0
- data/lib/legate/session_service/in_memory.rb +188 -0
- data/lib/legate/tool/metadata_dsl.rb +122 -0
- data/lib/legate/tool.rb +276 -0
- data/lib/legate/tool_code_generator.rb +103 -0
- data/lib/legate/tool_context.rb +350 -0
- data/lib/legate/tool_loader.rb +39 -0
- data/lib/legate/tool_registry.rb +73 -0
- data/lib/legate/tool_result.rb +61 -0
- data/lib/legate/tools/agent_tool.rb +187 -0
- data/lib/legate/tools/base/http_client.rb +319 -0
- data/lib/legate/tools/base/safe_url.rb +56 -0
- data/lib/legate/tools/base_async_job_tool.rb +91 -0
- data/lib/legate/tools/calculator.rb +89 -0
- data/lib/legate/tools/cat_facts.rb +81 -0
- data/lib/legate/tools/check_job_status_tool.rb +48 -0
- data/lib/legate/tools/current_time_tool.rb +64 -0
- data/lib/legate/tools/echo.rb +43 -0
- data/lib/legate/tools/http_request_tool.rb +105 -0
- data/lib/legate/tools/random_number_tool.rb +64 -0
- data/lib/legate/tools/read_webpage_tool.rb +92 -0
- data/lib/legate/tools/sleepy_tool.rb +74 -0
- data/lib/legate/tools/webhook_tool.rb +146 -0
- data/lib/legate/version.rb +5 -0
- data/lib/legate/web/app.rb +984 -0
- data/lib/legate/web/public/css/main.css +4980 -0
- data/lib/legate/web/public/images/favicon-256.png +0 -0
- data/lib/legate/web/public/images/favicon-32.png +0 -0
- data/lib/legate/web/public/images/legate-logo-dark.png +0 -0
- data/lib/legate/web/public/images/legate-logo-light.png +0 -0
- data/lib/legate/web/public/js/legate.js +616 -0
- data/lib/legate/web/public/styles/main.scss +4402 -0
- data/lib/legate/web/routes/agent_authentication_routes.rb +530 -0
- data/lib/legate/web/routes/agent_definition_routes.rb +803 -0
- data/lib/legate/web/routes/agent_generator_routes.rb +80 -0
- data/lib/legate/web/routes/agent_interaction_routes.rb +734 -0
- data/lib/legate/web/routes/agent_runtime_routes.rb +323 -0
- data/lib/legate/web/routes/api_routes.rb +56 -0
- data/lib/legate/web/routes/authentication_routes.rb +1541 -0
- data/lib/legate/web/routes/core_routes.rb +111 -0
- data/lib/legate/web/routes/documentation_routes.rb +220 -0
- data/lib/legate/web/routes/tool_generator_routes.rb +81 -0
- data/lib/legate/web/routes/tools_ui_routes.rb +207 -0
- data/lib/legate/web/sass_compiler.rb +73 -0
- data/lib/legate/web/views/_active_session_info.slim +25 -0
- data/lib/legate/web/views/_activity_list.slim +55 -0
- data/lib/legate/web/views/_agent_card.slim +56 -0
- data/lib/legate/web/views/_agent_generator_modal.slim +382 -0
- data/lib/legate/web/views/_agent_status_controls.slim +71 -0
- data/lib/legate/web/views/_agent_tool_table.slim +74 -0
- data/lib/legate/web/views/_chat_message.slim +95 -0
- data/lib/legate/web/views/_display_agent_configuration.slim +26 -0
- data/lib/legate/web/views/_display_agent_description.slim +11 -0
- data/lib/legate/web/views/_display_agent_fallback.slim +15 -0
- data/lib/legate/web/views/_display_agent_hierarchy.slim +93 -0
- data/lib/legate/web/views/_display_agent_instruction.slim +17 -0
- data/lib/legate/web/views/_display_agent_mcp.slim +13 -0
- data/lib/legate/web/views/_display_agent_model.slim +17 -0
- data/lib/legate/web/views/_display_agent_name.slim +42 -0
- data/lib/legate/web/views/_display_agent_output_key.slim +26 -0
- data/lib/legate/web/views/_display_agent_type.slim +65 -0
- data/lib/legate/web/views/_edit_agent_configuration.slim +74 -0
- data/lib/legate/web/views/_edit_agent_description.slim +16 -0
- data/lib/legate/web/views/_edit_agent_fallback.slim +25 -0
- data/lib/legate/web/views/_edit_agent_hierarchy.slim +98 -0
- data/lib/legate/web/views/_edit_agent_instruction.slim +49 -0
- data/lib/legate/web/views/_edit_agent_mcp.slim +33 -0
- data/lib/legate/web/views/_edit_agent_model.slim +23 -0
- data/lib/legate/web/views/_edit_agent_output_key.slim +36 -0
- data/lib/legate/web/views/_edit_agent_tools.slim +40 -0
- data/lib/legate/web/views/_edit_agent_type.slim +67 -0
- data/lib/legate/web/views/_session_error.slim +4 -0
- data/lib/legate/web/views/_skeleton.slim +69 -0
- data/lib/legate/web/views/_tool_card.slim +9 -0
- data/lib/legate/web/views/_tool_generator_modal.slim +311 -0
- data/lib/legate/web/views/agent.slim +436 -0
- data/lib/legate/web/views/agent_auth.slim +562 -0
- data/lib/legate/web/views/agents.slim +369 -0
- data/lib/legate/web/views/auth.slim +112 -0
- data/lib/legate/web/views/auth_credential_detail.slim +327 -0
- data/lib/legate/web/views/auth_credentials.slim +261 -0
- data/lib/legate/web/views/auth_debug.slim +94 -0
- data/lib/legate/web/views/auth_mapping_detail.slim +151 -0
- data/lib/legate/web/views/auth_mapping_new.slim +123 -0
- data/lib/legate/web/views/auth_mappings.slim +120 -0
- data/lib/legate/web/views/auth_scheme_detail.slim +274 -0
- data/lib/legate/web/views/auth_schemes.slim +259 -0
- data/lib/legate/web/views/auth_test.slim +418 -0
- data/lib/legate/web/views/chat.slim +192 -0
- data/lib/legate/web/views/docs_index.slim +105 -0
- data/lib/legate/web/views/docs_show.slim +105 -0
- data/lib/legate/web/views/error_404.slim +5 -0
- data/lib/legate/web/views/index.slim +148 -0
- data/lib/legate/web/views/layout.slim +144 -0
- data/lib/legate/web/views/tool_detail.slim +87 -0
- data/lib/legate/web/views/tools.slim +50 -0
- data/lib/legate/web/webhook_listener.rb +367 -0
- data/lib/legate/web.rb +9 -0
- data/lib/legate.rb +220 -0
- data/public/docs/advanced/callbacks.md +828 -0
- data/public/docs/advanced/mcp_schema_conversion.md +59 -0
- data/public/docs/authentication/api_reference/config.md +210 -0
- data/public/docs/authentication/api_reference/credential.md +246 -0
- data/public/docs/authentication/api_reference/encryption.md +218 -0
- data/public/docs/authentication/api_reference/exchanged_credential.md +271 -0
- data/public/docs/authentication/api_reference/excon_middleware.md +175 -0
- data/public/docs/authentication/api_reference/index.md +30 -0
- data/public/docs/authentication/api_reference/scheme.md +250 -0
- data/public/docs/authentication/api_reference/schemes/api_key.md +175 -0
- data/public/docs/authentication/api_reference/schemes/google_service_account.md +221 -0
- data/public/docs/authentication/api_reference/schemes/http_bearer.md +169 -0
- data/public/docs/authentication/api_reference/schemes/oauth2.md +343 -0
- data/public/docs/authentication/api_reference/schemes/oidc.md +73 -0
- data/public/docs/authentication/api_reference/schemes/openid_connect.md +311 -0
- data/public/docs/authentication/api_reference/schemes/service_account.md +287 -0
- data/public/docs/authentication/api_reference/token_manager.md +221 -0
- data/public/docs/authentication/api_reference/token_store.md +146 -0
- data/public/docs/authentication/api_reference/tool_context_extension.md +166 -0
- data/public/docs/authentication/guides/api_key.md +190 -0
- data/public/docs/authentication/guides/bearer.md +172 -0
- data/public/docs/authentication/guides/configuration.md +255 -0
- data/public/docs/authentication/guides/custom_flow.md +523 -0
- data/public/docs/authentication/guides/index.md +24 -0
- data/public/docs/authentication/guides/migration.md +435 -0
- data/public/docs/authentication/guides/oauth2.md +252 -0
- data/public/docs/authentication/guides/oidc.md +241 -0
- data/public/docs/authentication/guides/overview.md +155 -0
- data/public/docs/authentication/guides/secure_storage.md +301 -0
- data/public/docs/authentication/guides/service_account.md +228 -0
- data/public/docs/authentication/guides/token_lifecycle.md +295 -0
- data/public/docs/authentication/guides/web_ui_integration.md +504 -0
- data/public/docs/authentication/index.md +58 -0
- data/public/docs/authentication/troubleshooting/credential_storage.md +550 -0
- data/public/docs/authentication/troubleshooting/environment_variables.md +540 -0
- data/public/docs/authentication/troubleshooting/index.md +11 -0
- data/public/docs/authentication/troubleshooting/oauth2_issues.md +220 -0
- data/public/docs/authentication/troubleshooting/oidc_issues.md +412 -0
- data/public/docs/authentication/troubleshooting/token_refresh.md +338 -0
- data/public/docs/cli/legate_cli_usage.md +363 -0
- data/public/docs/core_concepts/legate_agent_lifecycle.md +124 -0
- data/public/docs/core_concepts/legate_architecture_overview.md +110 -0
- data/public/docs/core_concepts/legate_configuration.md +116 -0
- data/public/docs/core_concepts/legate_definition_store.md +102 -0
- data/public/docs/core_concepts/legate_planner.md +94 -0
- data/public/docs/core_concepts/legate_session_service.md +104 -0
- data/public/docs/error_handling/legate_error_handling.md +122 -0
- data/public/docs/examples.md +199 -0
- data/public/docs/getting_started.md +111 -0
- data/public/docs/guides/agentic_agents.md +137 -0
- data/public/docs/guides/ai_code_generators.md +437 -0
- data/public/docs/guides/auto_loading.md +326 -0
- data/public/docs/guides/configuring_agent_webhooks.md +219 -0
- data/public/docs/guides/http_client_usage.md +264 -0
- data/public/docs/guides/llm_providers.md +137 -0
- data/public/docs/guides/mcp_client_integration.md +232 -0
- data/public/docs/guides/mcp_server_exposure.md +206 -0
- data/public/docs/guides/rails_integration.md +128 -0
- data/public/docs/guides/sending_outbound_webhooks.md +227 -0
- data/public/docs/guides/streaming.md +112 -0
- data/public/docs/guides/webhooks.md +288 -0
- data/public/docs/introduction.md +51 -0
- data/public/docs/multi_agent_systems/advanced_features.md +57 -0
- data/public/docs/multi_agent_systems/agent_delegation.md +190 -0
- data/public/docs/multi_agent_systems/agent_hierarchy.md +49 -0
- data/public/docs/multi_agent_systems/state_management.md +47 -0
- data/public/docs/multi_agent_systems/workflow_agents.md +72 -0
- data/public/docs/tools/legate_built_in_tools.md +332 -0
- data/public/docs/tools/legate_tools_and_registry.md +263 -0
- data/public/docs/web_ui/legate_web_ui.md +137 -0
- metadata +823 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'jwt'
|
|
4
|
+
require 'json'
|
|
5
|
+
require 'net/http'
|
|
6
|
+
require 'base64'
|
|
7
|
+
require 'time'
|
|
8
|
+
require_relative 'service_account'
|
|
9
|
+
|
|
10
|
+
module Legate
|
|
11
|
+
module Auth
|
|
12
|
+
module Schemes
|
|
13
|
+
# GoogleServiceAccount implements authentication for Google service accounts
|
|
14
|
+
# using JWT assertions for OAuth 2.0 token exchange
|
|
15
|
+
class GoogleServiceAccount < ServiceAccount
|
|
16
|
+
# Default token URL for Google service accounts
|
|
17
|
+
GOOGLE_TOKEN_URL = 'https://oauth2.googleapis.com/token'
|
|
18
|
+
|
|
19
|
+
# Initialize a new GoogleServiceAccount scheme
|
|
20
|
+
# @param audience [String, nil] The audience for the JWT (defaults to token URL)
|
|
21
|
+
# @param scopes [Array<String>, String, nil] The requested scopes
|
|
22
|
+
# @param token_url [String] The URL for token exchange
|
|
23
|
+
# @param token_lifetime [Integer] The token lifetime in seconds
|
|
24
|
+
def initialize(audience: nil, scopes: nil, token_url: GOOGLE_TOKEN_URL, token_lifetime: 3600)
|
|
25
|
+
super(
|
|
26
|
+
token_url: token_url,
|
|
27
|
+
audience: audience || token_url,
|
|
28
|
+
scopes: scopes,
|
|
29
|
+
token_lifetime: token_lifetime
|
|
30
|
+
)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# @return [Symbol] The scheme type
|
|
34
|
+
def scheme_type
|
|
35
|
+
:google_service_account
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Fetch a new token using the Google service account
|
|
39
|
+
# @param credential [Legate::Auth::Credential] The credential with service account info
|
|
40
|
+
# @return [Legate::Auth::ExchangedCredential] The exchanged credential with the token
|
|
41
|
+
# @raise [Legate::Auth::TokenExchangeError] If token exchange fails
|
|
42
|
+
def fetch_token(credential)
|
|
43
|
+
# Verify credential type
|
|
44
|
+
raise Legate::Auth::CredentialError, 'Invalid credential type for service account' unless credential.is_a?(Legate::Auth::Credential)
|
|
45
|
+
|
|
46
|
+
# Extract service account key from credential
|
|
47
|
+
service_account_key = get_service_account_key(credential)
|
|
48
|
+
|
|
49
|
+
# Create and sign the JWT
|
|
50
|
+
jwt = create_signed_jwt(service_account_key)
|
|
51
|
+
|
|
52
|
+
# Exchange the JWT for an access token
|
|
53
|
+
token_response = exchange_jwt_for_token(jwt)
|
|
54
|
+
|
|
55
|
+
# Create an exchanged credential with the token information
|
|
56
|
+
Legate::Auth::ExchangedCredential.new(
|
|
57
|
+
auth_type: :google_service_account,
|
|
58
|
+
access_token: token_response[:access_token],
|
|
59
|
+
expires_in: token_response[:expires_in],
|
|
60
|
+
token_type: token_response[:token_type],
|
|
61
|
+
scope: token_response[:scope]
|
|
62
|
+
)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
# Create and sign a JWT token for Google service account authentication
|
|
68
|
+
# @param service_account_key [Hash] The service account key data
|
|
69
|
+
# @return [String] The signed JWT
|
|
70
|
+
# @raise [Legate::Auth::TokenExchangeError] If JWT creation fails
|
|
71
|
+
def create_signed_jwt(service_account_key)
|
|
72
|
+
# Verify essential fields in the service account key
|
|
73
|
+
required_fields = %i[client_email private_key type]
|
|
74
|
+
missing_fields = required_fields.reject { |field| service_account_key.key?(field) }
|
|
75
|
+
|
|
76
|
+
raise Legate::Auth::CredentialError, "Service account key missing required fields: #{missing_fields.join(', ')}" unless missing_fields.empty?
|
|
77
|
+
|
|
78
|
+
# Verify this is a service account key
|
|
79
|
+
raise Legate::Auth::CredentialError, "Invalid key type: #{service_account_key[:type]}, expected 'service_account'" unless service_account_key[:type] == 'service_account'
|
|
80
|
+
|
|
81
|
+
# Create the JWT claim set
|
|
82
|
+
now = Time.now.to_i
|
|
83
|
+
claim_set = {
|
|
84
|
+
iss: service_account_key[:client_email],
|
|
85
|
+
aud: @audience,
|
|
86
|
+
exp: now + @token_lifetime,
|
|
87
|
+
iat: now
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
# Add scopes if present
|
|
91
|
+
claim_set[:scope] = @scopes.join(' ') if @scopes && !@scopes.empty?
|
|
92
|
+
|
|
93
|
+
# Add subject (sub) if present in credential
|
|
94
|
+
claim_set[:sub] = service_account_key[:sub] if service_account_key[:sub]
|
|
95
|
+
|
|
96
|
+
begin
|
|
97
|
+
# Create the JWT
|
|
98
|
+
private_key = OpenSSL::PKey::RSA.new(service_account_key[:private_key])
|
|
99
|
+
|
|
100
|
+
JWT.encode(claim_set, private_key, 'RS256', { typ: 'JWT' })
|
|
101
|
+
rescue StandardError => e
|
|
102
|
+
raise Legate::Auth::TokenExchangeError, "Failed to create JWT: #{e.message}"
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# File: lib/legate/auth/schemes/http_bearer.rb
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative '../scheme'
|
|
5
|
+
require_relative '../error'
|
|
6
|
+
require_relative '../credential'
|
|
7
|
+
require_relative '../exchanged_credential'
|
|
8
|
+
|
|
9
|
+
module Legate
|
|
10
|
+
module Auth
|
|
11
|
+
module Schemes
|
|
12
|
+
# HTTP Bearer authentication scheme.
|
|
13
|
+
# This scheme applies a bearer token to requests via the Authorization header.
|
|
14
|
+
class HTTPBearer < Scheme
|
|
15
|
+
# Get the type of authentication scheme
|
|
16
|
+
# @return [Symbol] The scheme type identifier
|
|
17
|
+
def scheme_type
|
|
18
|
+
:http_bearer
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Apply authentication to a request
|
|
22
|
+
# @param request [Hash] The request hash to modify
|
|
23
|
+
# @param credential [Legate::Auth::Credential, Legate::Auth::ExchangedCredential] The credential to use
|
|
24
|
+
# @return [Hash] The modified request with authentication applied
|
|
25
|
+
# @raise [Legate::Auth::Error] If the bearer token cannot be applied
|
|
26
|
+
def apply_to_request(request, credential)
|
|
27
|
+
# Create a deep copy of the request to avoid modifying the original
|
|
28
|
+
request_copy = Marshal.load(Marshal.dump(request))
|
|
29
|
+
|
|
30
|
+
# Handle the case where we get a stack object from Excon
|
|
31
|
+
if request_copy.is_a?(Hash)
|
|
32
|
+
if request_copy[:stack]
|
|
33
|
+
# Extract the data from stack (Excon middleware format)
|
|
34
|
+
%i[scheme method path host port query].each do |key|
|
|
35
|
+
request_copy[key] = request_copy[:stack][key] if request_copy[:stack][key] && !request_copy[key]
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Ensure headers hash exists
|
|
40
|
+
request_copy[:headers] ||= {}
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Extract the bearer token from the credential
|
|
44
|
+
bearer_token = extract_bearer_token(credential)
|
|
45
|
+
raise Legate::Auth::Error, 'Bearer token not found in credential' unless bearer_token
|
|
46
|
+
|
|
47
|
+
# Apply the bearer token to the Authorization header
|
|
48
|
+
validate_header_value!(bearer_token, 'Bearer token')
|
|
49
|
+
request_copy[:headers]['Authorization'] = "Bearer #{bearer_token}"
|
|
50
|
+
request_copy
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Exchange a credential for a token
|
|
54
|
+
# @param credential [Legate::Auth::Credential] The credential to exchange
|
|
55
|
+
# @return [Legate::Auth::ExchangedCredential] The exchanged token
|
|
56
|
+
def exchange_token(credential)
|
|
57
|
+
# For bearer tokens, we simply create a "token" that wraps the bearer token
|
|
58
|
+
# This is useful for token management consistency
|
|
59
|
+
bearer_token = extract_bearer_token(credential)
|
|
60
|
+
raise Legate::Auth::TokenExchangeError, 'Bearer token not found in credential' unless bearer_token
|
|
61
|
+
|
|
62
|
+
# Create a simple exchanged credential that never expires
|
|
63
|
+
Legate::Auth::ExchangedCredential.new(
|
|
64
|
+
auth_type: :http_bearer,
|
|
65
|
+
access_token: bearer_token
|
|
66
|
+
)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Get hash representation of the scheme
|
|
70
|
+
# @return [Hash] Scheme configuration as a hash
|
|
71
|
+
def to_h
|
|
72
|
+
{
|
|
73
|
+
type: scheme_type
|
|
74
|
+
}
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
# Extract the bearer token from a credential
|
|
80
|
+
# @param credential [Legate::Auth::Credential, Legate::Auth::ExchangedCredential] The credential
|
|
81
|
+
# @return [String, nil] The bearer token or nil if not found
|
|
82
|
+
def extract_bearer_token(credential)
|
|
83
|
+
# First try bearer_token
|
|
84
|
+
return credential[:bearer_token] if credential[:bearer_token]
|
|
85
|
+
|
|
86
|
+
# Next try access_token
|
|
87
|
+
return credential[:access_token] if credential[:access_token]
|
|
88
|
+
|
|
89
|
+
# Finally try token
|
|
90
|
+
credential[:token]
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Alias HTTPBearer as HttpBearer for backward compatibility
|
|
95
|
+
HttpBearer = HTTPBearer
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
# File: lib/legate/auth/schemes/oauth2.rb
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'oauth2'
|
|
5
|
+
require 'securerandom'
|
|
6
|
+
require 'digest'
|
|
7
|
+
require 'base64'
|
|
8
|
+
require_relative '../scheme'
|
|
9
|
+
require_relative '../error'
|
|
10
|
+
require_relative '../exchanged_credential'
|
|
11
|
+
|
|
12
|
+
module Legate
|
|
13
|
+
module Auth
|
|
14
|
+
module Schemes
|
|
15
|
+
# Implements OAuth 2.0 authentication
|
|
16
|
+
# Supports authorization code flow, client credentials flow, and token refresh
|
|
17
|
+
class OAuth2 < Legate::Auth::Scheme
|
|
18
|
+
# @return [String] The URL for the authorization endpoint
|
|
19
|
+
attr_reader :authorization_url
|
|
20
|
+
|
|
21
|
+
# @return [String] The URL for the token endpoint
|
|
22
|
+
attr_reader :token_url
|
|
23
|
+
|
|
24
|
+
# @return [Array<String>] The requested scopes
|
|
25
|
+
attr_reader :scopes
|
|
26
|
+
|
|
27
|
+
# @return [Boolean] Whether to use PKCE
|
|
28
|
+
attr_reader :use_pkce
|
|
29
|
+
|
|
30
|
+
# @return [Hash, nil] Additional parameters for authorization requests
|
|
31
|
+
attr_reader :additional_params
|
|
32
|
+
|
|
33
|
+
# @return [String, nil] The URL for the revocation endpoint
|
|
34
|
+
attr_reader :revocation_url
|
|
35
|
+
|
|
36
|
+
# Initialize a new OAuth2 scheme
|
|
37
|
+
# @param authorization_url [String, nil] The authorization URL (optional for non-interactive flows)
|
|
38
|
+
# @param token_url [String, nil] The token URL (optional for testing)
|
|
39
|
+
# @param scopes [Array<String>, String, nil] The requested scopes
|
|
40
|
+
# @param use_pkce [Boolean] Whether to use PKCE
|
|
41
|
+
# @param additional_params [Hash, nil] Additional parameters for authorization requests
|
|
42
|
+
# @param revocation_url [String, nil] The URL for the revocation endpoint
|
|
43
|
+
def initialize(*args, authorization_url: nil, token_url: nil, scopes: nil, use_pkce: true, additional_params: nil, revocation_url: nil, **kwargs)
|
|
44
|
+
# Handle positional hash parameter (for backward compatibility with child classes like OpenIDConnect)
|
|
45
|
+
if args.length == 1 && args[0].is_a?(Hash)
|
|
46
|
+
config = args[0]
|
|
47
|
+
authorization_url ||= config[:authorization_url]
|
|
48
|
+
token_url ||= config[:token_url]
|
|
49
|
+
scopes ||= config[:scopes] || config[:scope]
|
|
50
|
+
use_pkce = if config.key?(:use_pkce)
|
|
51
|
+
config[:use_pkce]
|
|
52
|
+
else
|
|
53
|
+
use_pkce.nil? || use_pkce
|
|
54
|
+
end
|
|
55
|
+
additional_params ||= config[:additional_params]
|
|
56
|
+
revocation_url ||= config[:revocation_url]
|
|
57
|
+
|
|
58
|
+
# Extract additional config values
|
|
59
|
+
kwargs[:client_id] ||= config[:client_id]
|
|
60
|
+
kwargs[:client_secret] ||= config[:client_secret]
|
|
61
|
+
kwargs[:redirect_uri] ||= config[:redirect_uri]
|
|
62
|
+
|
|
63
|
+
# Add any remaining config keys to kwargs
|
|
64
|
+
config.each do |k, v|
|
|
65
|
+
unless %i[authorization_url token_url scopes scope use_pkce
|
|
66
|
+
additional_params revocation_url client_id client_secret redirect_uri].include?(k)
|
|
67
|
+
kwargs[k] ||= v
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
@authorization_url = authorization_url
|
|
73
|
+
@token_url = token_url
|
|
74
|
+
@scopes = parse_scopes(scopes)
|
|
75
|
+
# Explicitly handle boolean type for use_pkce - preserve false values
|
|
76
|
+
@use_pkce = use_pkce
|
|
77
|
+
@additional_params = additional_params
|
|
78
|
+
@revocation_url = revocation_url
|
|
79
|
+
|
|
80
|
+
# Handle additional parameters which might be passed by derived classes
|
|
81
|
+
@client_id = kwargs[:client_id]
|
|
82
|
+
@client_secret = kwargs[:client_secret]
|
|
83
|
+
@redirect_uri = kwargs[:redirect_uri]
|
|
84
|
+
|
|
85
|
+
# Remaining options
|
|
86
|
+
@options = kwargs.reject { |k, _| %i[client_id client_secret redirect_uri].include?(k) }
|
|
87
|
+
|
|
88
|
+
# Always validate when this is the base class, not a derived class
|
|
89
|
+
validate! if self.class == Legate::Auth::Schemes::OAuth2
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# @return [Symbol] The scheme type
|
|
93
|
+
def scheme_type
|
|
94
|
+
:oauth2
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Validates the scheme configuration
|
|
98
|
+
# @raise [Legate::Auth::SchemeValidationError] If the configuration is invalid
|
|
99
|
+
def validate!
|
|
100
|
+
# Check if we're in test environment and whether to force validation
|
|
101
|
+
in_test = ENV['RSPEC_ENV'] == 'test'
|
|
102
|
+
force_validate = ENV['FORCE_VALIDATE'] == 'true'
|
|
103
|
+
|
|
104
|
+
# Only skip validation in test environment if FORCE_VALIDATE is not true
|
|
105
|
+
return if in_test && !force_validate
|
|
106
|
+
|
|
107
|
+
raise Legate::Auth::SchemeValidationError, 'Authorization URL is required' if @authorization_url.nil? || @authorization_url.to_s.strip.empty?
|
|
108
|
+
|
|
109
|
+
return unless @token_url.nil? || @token_url.to_s.strip.empty?
|
|
110
|
+
|
|
111
|
+
raise Legate::Auth::SchemeValidationError, 'Token URL is required'
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Build the authorization URI for the OAuth2 flow
|
|
115
|
+
# @param config [Legate::Auth::Config] The authentication configuration
|
|
116
|
+
# @param redirect_uri [String, nil] The redirect URI for the authorization request
|
|
117
|
+
# @param state [String, nil] A state parameter for CSRF protection
|
|
118
|
+
# @return [Hash] The authorization URI and any additional parameters (like PKCE code verifier)
|
|
119
|
+
def build_authorization_uri(config, redirect_uri = nil, state = nil)
|
|
120
|
+
# Get credentials from the config
|
|
121
|
+
credential = config.credential
|
|
122
|
+
|
|
123
|
+
# Generate state for CSRF protection if not provided
|
|
124
|
+
state ||= SecureRandom.hex(16)
|
|
125
|
+
|
|
126
|
+
# Build the authorization URL with parameters
|
|
127
|
+
client_id = credential[:client_id, resolve_env: true]
|
|
128
|
+
|
|
129
|
+
# Create the basic parameters
|
|
130
|
+
params = {
|
|
131
|
+
'client_id' => client_id,
|
|
132
|
+
'response_type' => 'code',
|
|
133
|
+
'redirect_uri' => redirect_uri,
|
|
134
|
+
'state' => state
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
# Add scopes if present
|
|
138
|
+
params['scope'] = @scopes.join(' ') if @scopes && !@scopes.empty?
|
|
139
|
+
|
|
140
|
+
# Result hash that will be returned
|
|
141
|
+
result = {
|
|
142
|
+
uri: nil, # Will be set below
|
|
143
|
+
state: state
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
# Add PKCE if enabled - directly check the instance variable
|
|
147
|
+
# Only add PKCE if @use_pkce is not false (nil would default to true)
|
|
148
|
+
if @use_pkce != false
|
|
149
|
+
code_verifier = SecureRandom.alphanumeric(64)
|
|
150
|
+
code_challenge = generate_code_challenge(code_verifier)
|
|
151
|
+
|
|
152
|
+
params['code_challenge'] = code_challenge
|
|
153
|
+
params['code_challenge_method'] = 'S256'
|
|
154
|
+
|
|
155
|
+
result[:pkce] = { code_verifier: code_verifier }
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Add any additional parameters
|
|
159
|
+
params.merge!(@additional_params) if @additional_params
|
|
160
|
+
|
|
161
|
+
# Remove nil values
|
|
162
|
+
params.compact!
|
|
163
|
+
|
|
164
|
+
# Build the query string
|
|
165
|
+
query = URI.encode_www_form(params)
|
|
166
|
+
|
|
167
|
+
# Join with the authorization URL
|
|
168
|
+
result[:uri] = "#{@authorization_url}?#{query}"
|
|
169
|
+
|
|
170
|
+
result
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Applies the OAuth token to a request
|
|
174
|
+
# @param request [Hash] The request to apply the token to
|
|
175
|
+
# @param credential [Legate::Auth::Credential, Legate::Auth::ExchangedCredential] The credential with the token
|
|
176
|
+
# @return [Hash] The updated request
|
|
177
|
+
# @raise [Legate::Auth::CredentialError] If the credential is missing the token
|
|
178
|
+
def apply_to_request(request, credential)
|
|
179
|
+
raise Legate::Auth::CredentialError, 'Expected an exchanged credential' unless credential.is_a?(Legate::Auth::ExchangedCredential)
|
|
180
|
+
|
|
181
|
+
access_token = credential[:access_token]
|
|
182
|
+
raise Legate::Auth::CredentialError, 'Access token is missing from credential' unless access_token
|
|
183
|
+
|
|
184
|
+
# Apply the access token to the Authorization header
|
|
185
|
+
validate_header_value!(access_token, 'OAuth2 access token')
|
|
186
|
+
request[:headers] ||= {}
|
|
187
|
+
request[:headers]['Authorization'] = "Bearer #{access_token}"
|
|
188
|
+
request
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Exchanges an authorization code for tokens
|
|
192
|
+
# @param config [Legate::Auth::Config] The authentication configuration
|
|
193
|
+
# @param credential [Legate::Auth::Credential] The credential with client information
|
|
194
|
+
# @return [Legate::Auth::ExchangedCredential] The exchanged credential with tokens
|
|
195
|
+
# @raise [Legate::Auth::TokenExchangeError] If token exchange fails
|
|
196
|
+
def exchange_token(config, credential)
|
|
197
|
+
raise Legate::Auth::TokenExchangeError, 'Response URI is required for token exchange' unless config.response_uri
|
|
198
|
+
|
|
199
|
+
# Extract the code from the response URI
|
|
200
|
+
uri = URI.parse(config.response_uri)
|
|
201
|
+
params = CGI.parse(uri.query || '')
|
|
202
|
+
code = params['code']&.first
|
|
203
|
+
|
|
204
|
+
raise Legate::Auth::TokenExchangeError, 'Authorization code not found in response URI' unless code
|
|
205
|
+
|
|
206
|
+
# Verify the state parameter to prevent CSRF attacks
|
|
207
|
+
raise Legate::Auth::TokenExchangeError, 'State parameter mismatch' if config.state && params['state']&.first != config.state
|
|
208
|
+
|
|
209
|
+
begin
|
|
210
|
+
# Create an OAuth2 client
|
|
211
|
+
oauth_client = create_oauth_client(credential)
|
|
212
|
+
|
|
213
|
+
# Exchange the code for tokens
|
|
214
|
+
auth_params = {
|
|
215
|
+
redirect_uri: config.redirect_uri,
|
|
216
|
+
code: code
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
# Add PKCE code_verifier if available
|
|
220
|
+
auth_params[:code_verifier] = config.pkce[:code_verifier] if config.pkce && config.pkce[:code_verifier]
|
|
221
|
+
|
|
222
|
+
token = oauth_client.auth_code.get_token(code, auth_params)
|
|
223
|
+
|
|
224
|
+
# Create an exchanged credential from the token response
|
|
225
|
+
Legate::Auth::ExchangedCredential.new(
|
|
226
|
+
auth_type: scheme_type,
|
|
227
|
+
access_token: token.token,
|
|
228
|
+
refresh_token: token.refresh_token,
|
|
229
|
+
token_type: token.params['token_type'],
|
|
230
|
+
expires_at: token.expires_at ? Time.at(token.expires_at) : nil,
|
|
231
|
+
expires_in: token.expires_in,
|
|
232
|
+
scope: token.params['scope']
|
|
233
|
+
)
|
|
234
|
+
rescue ::OAuth2::Error => e
|
|
235
|
+
raise Legate::Auth::TokenExchangeError, "OAuth2 token exchange failed: #{e.message}"
|
|
236
|
+
rescue StandardError => e
|
|
237
|
+
raise Legate::Auth::TokenExchangeError, "Token exchange failed: #{e.message}"
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# Refreshes an access token using a refresh token
|
|
242
|
+
# @param exchanged_credential [Legate::Auth::ExchangedCredential] The credential with the refresh token
|
|
243
|
+
# @param credential [Legate::Auth::Credential] The original credential with client information
|
|
244
|
+
# @return [Legate::Auth::ExchangedCredential] The refreshed credential
|
|
245
|
+
# @raise [Legate::Auth::TokenRefreshError] If token refresh fails
|
|
246
|
+
def refresh_token(exchanged_credential, credential)
|
|
247
|
+
refresh_token = exchanged_credential[:refresh_token]
|
|
248
|
+
|
|
249
|
+
raise Legate::Auth::TokenRefreshError, 'Refresh token is missing from credential' unless refresh_token && !refresh_token.empty?
|
|
250
|
+
|
|
251
|
+
begin
|
|
252
|
+
# Create an OAuth2 client
|
|
253
|
+
oauth_client = create_oauth_client(credential)
|
|
254
|
+
|
|
255
|
+
# Create a token object with the refresh token
|
|
256
|
+
token = ::OAuth2::AccessToken.from_hash(oauth_client, {
|
|
257
|
+
refresh_token: refresh_token,
|
|
258
|
+
expires_at: exchanged_credential[:expires_at]&.to_i
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
# Refresh the token
|
|
262
|
+
refreshed_token = token.refresh!
|
|
263
|
+
|
|
264
|
+
# Create a new exchanged credential with the refreshed token
|
|
265
|
+
Legate::Auth::ExchangedCredential.new(
|
|
266
|
+
auth_type: scheme_type,
|
|
267
|
+
access_token: refreshed_token.token,
|
|
268
|
+
refresh_token: refreshed_token.refresh_token || refresh_token,
|
|
269
|
+
token_type: refreshed_token.params['token_type'],
|
|
270
|
+
expires_at: refreshed_token.expires_at ? Time.at(refreshed_token.expires_at) : nil,
|
|
271
|
+
expires_in: refreshed_token.expires_in,
|
|
272
|
+
scope: refreshed_token.params['scope']
|
|
273
|
+
)
|
|
274
|
+
rescue ::OAuth2::Error => e
|
|
275
|
+
raise Legate::Auth::TokenRefreshError, "OAuth2 token refresh failed: #{e.message}"
|
|
276
|
+
rescue StandardError => e
|
|
277
|
+
raise Legate::Auth::TokenRefreshError, "Token refresh failed: #{e.message}"
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# Exchange client credentials for an access token (client credentials flow)
|
|
282
|
+
# @param credential [Legate::Auth::Credential] The credential with client information
|
|
283
|
+
# @return [Legate::Auth::ExchangedCredential] The exchanged credential with tokens
|
|
284
|
+
# @raise [Legate::Auth::TokenExchangeError] If token exchange fails
|
|
285
|
+
def client_credentials_token(credential)
|
|
286
|
+
# Create an OAuth2 client
|
|
287
|
+
oauth_client = create_oauth_client(credential)
|
|
288
|
+
|
|
289
|
+
# Request a token using the client credentials flow
|
|
290
|
+
auth_params = {}
|
|
291
|
+
auth_params[:scope] = @scopes.join(' ') if @scopes && !@scopes.empty?
|
|
292
|
+
|
|
293
|
+
token = oauth_client.client_credentials.get_token(auth_params)
|
|
294
|
+
|
|
295
|
+
# Create an exchanged credential from the token response
|
|
296
|
+
Legate::Auth::ExchangedCredential.new(
|
|
297
|
+
auth_type: scheme_type,
|
|
298
|
+
access_token: token.token,
|
|
299
|
+
token_type: token.params['token_type'],
|
|
300
|
+
expires_at: token.expires_at ? Time.at(token.expires_at) : nil,
|
|
301
|
+
expires_in: token.expires_in,
|
|
302
|
+
scope: token.params['scope']
|
|
303
|
+
)
|
|
304
|
+
rescue ::OAuth2::Error => e
|
|
305
|
+
raise Legate::Auth::TokenExchangeError, "OAuth2 client credentials exchange failed: #{e.message}"
|
|
306
|
+
rescue StandardError => e
|
|
307
|
+
raise Legate::Auth::TokenExchangeError, "Client credentials exchange failed: #{e.message}"
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
# Password flow for getting an access token (resource owner password credentials flow)
|
|
311
|
+
# @param credential [Legate::Auth::Credential] The credential with client information
|
|
312
|
+
# @param username [String] The resource owner's username
|
|
313
|
+
# @param password [String] The resource owner's password
|
|
314
|
+
# @return [Legate::Auth::ExchangedCredential] The exchanged credential with tokens
|
|
315
|
+
# @raise [Legate::Auth::TokenExchangeError] If token exchange fails
|
|
316
|
+
def password_token(credential, username, password)
|
|
317
|
+
# Create an OAuth2 client
|
|
318
|
+
oauth_client = create_oauth_client(credential)
|
|
319
|
+
|
|
320
|
+
# Request a token using the password flow
|
|
321
|
+
auth_params = {}
|
|
322
|
+
auth_params[:scope] = @scopes.join(' ') if @scopes && !@scopes.empty?
|
|
323
|
+
|
|
324
|
+
token = oauth_client.password.get_token(username, password, auth_params)
|
|
325
|
+
|
|
326
|
+
# Create an exchanged credential from the token response
|
|
327
|
+
Legate::Auth::ExchangedCredential.new(
|
|
328
|
+
auth_type: scheme_type,
|
|
329
|
+
access_token: token.token,
|
|
330
|
+
refresh_token: token.refresh_token,
|
|
331
|
+
token_type: token.params['token_type'],
|
|
332
|
+
expires_at: token.expires_at ? Time.at(token.expires_at) : nil,
|
|
333
|
+
expires_in: token.expires_in,
|
|
334
|
+
scope: token.params['scope']
|
|
335
|
+
)
|
|
336
|
+
rescue ::OAuth2::Error => e
|
|
337
|
+
raise Legate::Auth::TokenExchangeError, "OAuth2 password flow failed: #{e.message}"
|
|
338
|
+
rescue StandardError => e
|
|
339
|
+
raise Legate::Auth::TokenExchangeError, "Password flow failed: #{e.message}"
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
# @return [Boolean] Whether to use PKCE
|
|
343
|
+
attr_reader :use_pkce
|
|
344
|
+
|
|
345
|
+
private
|
|
346
|
+
|
|
347
|
+
# Parse scopes from string or array
|
|
348
|
+
# @param scopes [String, Array<String>, nil] The scopes to parse
|
|
349
|
+
# @return [Array<String>] The parsed scopes
|
|
350
|
+
def parse_scopes(scopes)
|
|
351
|
+
return [] if scopes.nil?
|
|
352
|
+
return scopes if scopes.is_a?(Array)
|
|
353
|
+
return scopes.split(/\s+/) if scopes.is_a?(String)
|
|
354
|
+
|
|
355
|
+
[]
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
# Generate a code challenge for PKCE
|
|
359
|
+
# @param code_verifier [String] The code verifier to use
|
|
360
|
+
# @return [String] The code challenge
|
|
361
|
+
def generate_code_challenge(code_verifier)
|
|
362
|
+
Base64.urlsafe_encode64(Digest::SHA256.digest(code_verifier), padding: false)
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
# Create an OAuth2 client
|
|
366
|
+
# @param credential [Legate::Auth::Credential] The credential with client information
|
|
367
|
+
# @return [OAuth2::Client] The OAuth2 client
|
|
368
|
+
def create_oauth_client(credential)
|
|
369
|
+
validate_auth_url!(@token_url, label: 'Token URL') if @token_url
|
|
370
|
+
client_id = credential[:client_id, resolve_env: true]
|
|
371
|
+
client_secret = credential[:client_secret, resolve_env: true]
|
|
372
|
+
|
|
373
|
+
# Create the OAuth2 client
|
|
374
|
+
::OAuth2::Client.new(
|
|
375
|
+
client_id,
|
|
376
|
+
client_secret,
|
|
377
|
+
site: determine_site_url,
|
|
378
|
+
authorize_url: @authorization_url,
|
|
379
|
+
token_url: @token_url,
|
|
380
|
+
auth_scheme: :basic_auth
|
|
381
|
+
)
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
# Determine the site URL from the token URL
|
|
385
|
+
# @return [String] The site URL
|
|
386
|
+
def determine_site_url
|
|
387
|
+
return nil unless @token_url
|
|
388
|
+
|
|
389
|
+
# Extract the scheme and host from the token URL
|
|
390
|
+
uri = URI.parse(@token_url)
|
|
391
|
+
"#{uri.scheme}://#{uri.host}"
|
|
392
|
+
end
|
|
393
|
+
end
|
|
394
|
+
end
|
|
395
|
+
end
|
|
396
|
+
end
|