diff --git a/lib/workos/sso.rb b/lib/workos/sso.rb index 080093d7..a6720503 100644 --- a/lib/workos/sso.rb +++ b/lib/workos/sso.rb @@ -3,6 +3,7 @@ # This file is auto-generated by oagen. Do not edit. require "json" +require "uri" module WorkOS class SSO @@ -106,6 +107,74 @@ def delete_connection( nil end + # Initiate SSO + # Builds the URL client-side; no HTTP request is made. + # @param provider_scopes [Array, nil] Additional scopes to request from the identity provider. Applicable when using OAuth or OpenID Connect connections. + # @param provider_query_params [Hash{String => String}, nil] Key/value pairs of query parameters to pass to the OAuth provider. Only applicable when using OAuth connections. + # @param client_id [String, nil] The unique identifier of the WorkOS environment client. Defaults to the client's configured client_id. + # @param domain [String, nil] Deprecated. Use `connection` or `organization` instead. Used to initiate SSO for a connection by domain. The domain must be associated with a connection in your WorkOS environment. + # @param provider [WorkOS::Types::SSOProvider, nil] Used to initiate OAuth authentication with various providers. + # @param redirect_uri [String] Where to redirect the user after they complete the authentication process. You must use one of the redirect URIs configured via the [Redirects](https://dashboard.workos.com/redirects) page on the dashboard. + # @param state [String, nil] An optional parameter that can be used to encode arbitrary information to help restore application state between redirects. If included, the redirect URI received from WorkOS will contain the exact `state` that was passed. + # @param connection [String, nil] Used to initiate SSO for a connection. The value should be a WorkOS connection ID. You can persist the WorkOS connection ID with application user or team identifiers. WorkOS will use the connection indicated by the connection parameter to direct the user to the corresponding IdP for authentication. + # @param organization [String, nil] Used to initiate SSO for an organization. The value should be a WorkOS organization ID. You can persist the WorkOS organization ID with application user or team identifiers. WorkOS will use the organization ID to determine the appropriate connection and the IdP to direct the user to for authentication. + # @param domain_hint [String, nil] Can be used to pre-fill the domain field when initiating authentication with Microsoft OAuth or with a Google SAML connection type. + # @param login_hint [String, nil] Can be used to pre-fill the username/email address field of the IdP sign-in page for the user, if you know their username ahead of time. Currently supported for OAuth, OpenID Connect, Okta, Entra ID, and custom SAML connections. + # @param nonce [String, nil] A random string generated by the client that is used to mitigate replay attacks. + # @param prompt [String, nil] If set to `login`, forces re-authentication at the identity provider. For SAML connections this sets `ForceAuthn="true"` in the SAML request. + # @return [String] + def get_authorization_url( + redirect_uri:, + provider_scopes: nil, + provider_query_params: nil, + client_id: nil, + domain: nil, + provider: nil, + state: nil, + connection: nil, + organization: nil, + domain_hint: nil, + login_hint: nil, + nonce: nil, + prompt: nil + ) + params = { + "provider_scopes" => provider_scopes, + "provider_query_params" => provider_query_params, + "client_id" => client_id, + "domain" => domain, + "provider" => provider, + "redirect_uri" => redirect_uri, + "state" => state, + "connection" => connection, + "organization" => organization, + "domain_hint" => domain_hint, + "login_hint" => login_hint, + "nonce" => nonce, + "prompt" => prompt + }.compact + params["provider_scopes"] = provider_scopes.join(",") unless provider_scopes.nil? + params["provider_query_params"] = JSON.generate(provider_query_params) unless provider_query_params.nil? + params["response_type"] = "code" + params["client_id"] = @client.client_id if !params.key?("client_id") && !@client.client_id.nil? + uri = URI.join(@client.base_url, "/sso/authorize") + uri.query = URI.encode_www_form(params) unless params.empty? + uri.to_s + end + + # Logout Redirect + # Builds the URL client-side; no HTTP request is made. + # @param token [String] The logout token returned from the [Logout Authorize](https://workos.com/docs/reference/sso/logout/authorize) endpoint. + # @return [String] + def get_logout_url(token:) + params = { + "token" => token + } + uri = URI.join(@client.base_url, "/sso/logout") + uri.query = URI.encode_www_form(params) unless params.empty? + uri.to_s + end + # Logout Authorize # @param profile_id [String] The unique ID of the profile to log out. # @param request_options [Hash] (see WorkOS::Types::RequestOptions) @@ -171,32 +240,10 @@ def get_profile_and_token( end # @oagen-ignore-start — non-spec helpers (hand-maintained) - # H14 — Build an SSO authorization URL (client-side, no HTTP call). - # Overrides the generated method which incorrectly hits the API. - def get_authorization_url(redirect_uri:, client_id: nil, state: nil, connection: nil, - organization: nil, provider: nil, domain_hint: nil, - login_hint: nil, nonce: nil, provider_scopes: nil, - provider_query_params: nil, **) - cid = client_id || @client.client_id - raise ArgumentError, "client_id is required (set on Client or pass explicitly)" if cid.nil? || cid.empty? - params = { - "client_id" => cid, - "redirect_uri" => redirect_uri, - "response_type" => "code", - "state" => state, - "connection" => connection, - "organization" => organization, - "provider" => provider, - "domain_hint" => domain_hint, - "login_hint" => login_hint, - "nonce" => nonce - }.compact - params["provider_scopes"] = Array(provider_scopes).join(",") if provider_scopes - if provider_query_params.is_a?(Hash) && !provider_query_params.empty? - params["provider_query_params"] = JSON.generate(provider_query_params) - end - build_url("/sso/authorize", params) - end + # H14 (sso_authorization_url) is provided by the generated + # `get_authorization_url` url-builder method, which accepts an optional + # per-call `client_id` override falling back to the client's configured + # value; no hand-maintained override is needed here. # H15 — SSO authorization URL with auto-generated PKCE pair + state. # Returns [url, code_verifier, state]. diff --git a/lib/workos/user_management.rb b/lib/workos/user_management.rb index a93f6e74..10feb88e 100644 --- a/lib/workos/user_management.rb +++ b/lib/workos/user_management.rb @@ -3,6 +3,7 @@ # This file is auto-generated by oagen. Do not edit. require "json" +require "uri" module WorkOS class UserManagement @@ -515,6 +516,70 @@ def authenticate_with_radar_sms_challenge( WorkOS::AuthenticateResponse.new(response.body) end + # Get an authorization URL + # Builds the URL client-side; no HTTP request is made. + # @param code_challenge_method [String, nil] The only valid PKCE code challenge method is `"S256"`. Required when specifying a `code_challenge`. + # @param code_challenge [String, nil] Code challenge derived from the code verifier used for the PKCE flow. + # @param domain_hint [String, nil] A domain hint for SSO connection lookup. + # @param connection_id [String, nil] The ID of an SSO connection to use for authentication. + # @param provider_query_params [Hash{String => String}, nil] Key/value pairs of query parameters to pass to the OAuth provider. + # @param provider_scopes [Array, nil] Additional OAuth scopes to request from the identity provider. + # @param invitation_token [String, nil] A token representing a user invitation to redeem during authentication. + # @param max_age [Integer, nil] Maximum allowable elapsed time, in seconds, since the user last actively authenticated. If the last authentication is older than this value, the user is prompted to re-authenticate; a value of `0` forces re-authentication. Only supported when the provider is `authkit`. + # @param screen_hint [WorkOS::Types::UserManagementAuthenticationScreenHint, nil] Used to specify which screen to display when the provider is `authkit`. + # @param login_hint [String, nil] A hint to the authorization server about the login identifier the user might use. + # @param provider [WorkOS::Types::UserManagementAuthenticationProvider, nil] The OAuth provider to authenticate with (e.g., GoogleOAuth, MicrosoftOAuth, GitHubOAuth). + # @param prompt [String, nil] Controls the authentication flow behavior for the user. + # @param state [String, nil] An opaque value used to maintain state between the request and the callback. + # @param organization_id [String, nil] The ID of the organization to authenticate the user against. + # @param redirect_uri [String] The callback URI where the authorization code will be sent after authentication. + # @param client_id [String, nil] The unique identifier of the WorkOS environment client. Defaults to the client's configured client_id. + # @return [String] + def get_authorization_url( + redirect_uri:, + code_challenge_method: nil, + code_challenge: nil, + domain_hint: nil, + connection_id: nil, + provider_query_params: nil, + provider_scopes: nil, + invitation_token: nil, + max_age: nil, + screen_hint: nil, + login_hint: nil, + provider: nil, + prompt: nil, + state: nil, + organization_id: nil, + client_id: nil + ) + params = { + "code_challenge_method" => code_challenge_method, + "code_challenge" => code_challenge, + "domain_hint" => domain_hint, + "connection_id" => connection_id, + "provider_query_params" => provider_query_params, + "provider_scopes" => provider_scopes, + "invitation_token" => invitation_token, + "max_age" => max_age, + "screen_hint" => screen_hint, + "login_hint" => login_hint, + "provider" => provider, + "prompt" => prompt, + "state" => state, + "organization_id" => organization_id, + "redirect_uri" => redirect_uri, + "client_id" => client_id + }.compact + params["provider_query_params"] = JSON.generate(provider_query_params) unless provider_query_params.nil? + params["provider_scopes"] = provider_scopes.join(",") unless provider_scopes.nil? + params["response_type"] = "code" + params["client_id"] = @client.client_id if !params.key?("client_id") && !@client.client_id.nil? + uri = URI.join(@client.base_url, "/user_management/authorize") + uri.query = URI.encode_www_form(params) unless params.empty? + uri.to_s + end + # Get device authorization URL # @param client_id [String] The WorkOS client ID for your application. # @param request_options [Hash] (see WorkOS::Types::RequestOptions) @@ -592,6 +657,24 @@ def get_radar_challenge( result end + # Logout + # Builds the URL client-side; no HTTP request is made. + # @param session_id [String] The ID of the session. This can be extracted from the `sid` claim of the access token. + # @param return_to [String, nil] The URL to redirect the user to after logout. + # @return [String] + def get_logout_url( + session_id:, + return_to: nil + ) + params = { + "session_id" => session_id, + "return_to" => return_to + }.compact + uri = URI.join(@client.base_url, "/user_management/sessions/logout") + uri.query = URI.encode_www_form(params) unless params.empty? + uri.to_s + end + # Revoke Session # @param session_id [String] The ID of the session to revoke. This can be extracted from the `sid` claim of the access token. # @param request_options [Hash] (see WorkOS::Types::RequestOptions) @@ -1690,32 +1773,10 @@ def get_jwks_url(client_id: nil) URI.join(base, "/sso/jwks/#{WorkOS::Util.encode_path(cid)}").to_s end - # H09 — Build an AuthKit authorization URL (client-side, no HTTP call). - # Overrides the generated get_authorization_url which hits the API. - def get_authorization_url(redirect_uri:, client_id: nil, provider: nil, connection_id: nil, - organization_id: nil, domain_hint: nil, login_hint: nil, - state: nil, screen_hint: nil, code_challenge: nil, - code_challenge_method: nil, prompt: nil, **) - cid = client_id || @client.client_id - raise ArgumentError, "client_id is required (set on Client or pass explicitly)" if cid.nil? || cid.empty? - raise ArgumentError, "provider, connection_id, or organization_id required" if provider.nil? && connection_id.nil? && organization_id.nil? - params = { - "client_id" => cid, - "redirect_uri" => redirect_uri, - "response_type" => "code", - "provider" => provider, - "connection_id" => connection_id, - "organization_id" => organization_id, - "domain_hint" => domain_hint, - "login_hint" => login_hint, - "state" => state, - "screen_hint" => screen_hint, - "code_challenge" => code_challenge, - "code_challenge_method" => code_challenge_method, - "prompt" => prompt - }.compact - build_url("/user_management/authorize", params) - end + # H09 (authkit_authorization_url) is provided by the generated + # `get_authorization_url` url-builder method, which accepts an optional + # per-call `client_id` override falling back to the client's configured + # value; no hand-maintained override is needed here. # H10 — AuthKit authorization URL with auto-generated PKCE + state. # Returns [url, code_verifier, state]. @@ -1773,24 +1834,9 @@ def authorize_device(client_id: nil, request_options: {}) # `authenticate_with_device_code` method (wraps /user_management/authenticate); # no hand-maintained override is needed here. - # Build the AuthKit logout redirect URL (client-side, no HTTP call). - # @param session_id [String] The session ID (from the `sid` claim of the access token). - # @param return_to [String, nil] URL to redirect the user to after session revocation. - # @return [String] - def get_logout_url(session_id:, return_to: nil) - params = {"session_id" => session_id} - params["return_to"] = return_to if return_to - build_url("/user_management/sessions/logout", params) - end - - private - - def build_url(path, params) - base = @client.base_url - uri = URI.join(base, path) - uri.query = URI.encode_www_form(params) - uri.to_s - end + # The AuthKit logout redirect URL is provided by the generated + # `get_logout_url` url-builder method; no hand-maintained override is + # needed here. # @oagen-ignore-end end end diff --git a/rbi/workos/sso.rbi b/rbi/workos/sso.rbi index 9c134fac..13aa23d2 100644 --- a/rbi/workos/sso.rbi +++ b/rbi/workos/sso.rbi @@ -40,6 +40,32 @@ module WorkOS end def delete_connection(id:, request_options:); end + sig do + params( + redirect_uri: String, + provider_scopes: T.nilable(T::Array[String]), + provider_query_params: T.nilable(T::Hash[String, String]), + client_id: T.nilable(String), + domain: T.nilable(String), + provider: T.nilable(String), + state: T.nilable(String), + connection: T.nilable(String), + organization: T.nilable(String), + domain_hint: T.nilable(String), + login_hint: T.nilable(String), + nonce: T.nilable(String), + prompt: T.nilable(String) + ).returns(String) + end + def get_authorization_url(redirect_uri:, provider_scopes:, provider_query_params:, client_id:, domain:, provider:, state:, connection:, organization:, domain_hint:, login_hint:, nonce:, prompt:); end + + sig do + params( + token: String + ).returns(String) + end + def get_logout_url(token:); end + sig do params( profile_id: String, diff --git a/rbi/workos/user_management.rbi b/rbi/workos/user_management.rbi index a245ebbf..159af254 100644 --- a/rbi/workos/user_management.rbi +++ b/rbi/workos/user_management.rbi @@ -62,6 +62,28 @@ module WorkOS end def create_authenticate(client_id:, grant_type:, code:, client_secret:, code_verifier:, invitation_token:, ip_address:, device_id:, user_agent:, signals_id:, request_options:); end + sig do + params( + redirect_uri: String, + code_challenge_method: T.nilable(String), + code_challenge: T.nilable(String), + domain_hint: T.nilable(String), + connection_id: T.nilable(String), + provider_query_params: T.nilable(T::Hash[String, String]), + provider_scopes: T.nilable(T::Array[String]), + invitation_token: T.nilable(String), + max_age: T.nilable(Integer), + screen_hint: T.nilable(String), + login_hint: T.nilable(String), + provider: T.nilable(String), + prompt: T.nilable(String), + state: T.nilable(String), + organization_id: T.nilable(String), + client_id: T.nilable(String) + ).returns(String) + end + def get_authorization_url(redirect_uri:, code_challenge_method:, code_challenge:, domain_hint:, connection_id:, provider_query_params:, provider_scopes:, invitation_token:, max_age:, screen_hint:, login_hint:, provider:, prompt:, state:, organization_id:, client_id:); end + sig do params( client_id: String, @@ -90,6 +112,14 @@ module WorkOS end def get_radar_challenge(id:, request_options:); end + sig do + params( + session_id: String, + return_to: T.nilable(String) + ).returns(String) + end + def get_logout_url(session_id:, return_to:); end + sig do params( session_id: String, diff --git a/test/workos/test_authkit_helpers.rb b/test/workos/test_authkit_helpers.rb index 076dd533..4706cd29 100644 --- a/test/workos/test_authkit_helpers.rb +++ b/test/workos/test_authkit_helpers.rb @@ -26,19 +26,21 @@ def test_get_authorization_url_returns_string_no_http assert_equal "/user_management/authorize", parsed.path end - def test_get_authorization_url_requires_client_id + def test_get_authorization_url_omits_client_id_when_unconfigured client = WorkOS::Client.new(api_key: "k", client_id: nil) - err = assert_raises(ArgumentError) do - client.user_management.get_authorization_url(redirect_uri: "x", provider: "GoogleOAuth") - end - assert_match(/client_id is required/, err.message) + url = client.user_management.get_authorization_url(redirect_uri: "x", provider: "GoogleOAuth") + params = URI.decode_www_form(URI.parse(url).query).to_h + refute params.key?("client_id") end - def test_get_authorization_url_requires_provider_or_connection_or_org - err = assert_raises(ArgumentError) do - @um.get_authorization_url(redirect_uri: "x") - end - assert_match(/provider, connection_id, or organization_id required/, err.message) + def test_get_authorization_url_explicit_client_id_overrides_configured + url = @um.get_authorization_url( + redirect_uri: "https://app.example.com/cb", + provider: "GoogleOAuth", + client_id: "client_override" + ) + params = URI.decode_www_form(URI.parse(url).query).to_h + assert_equal "client_override", params["client_id"] end # H10 @@ -57,6 +59,16 @@ def test_get_authorization_url_with_pkce_returns_url_verifier_state assert_equal expected_challenge, params["code_challenge"] end + def test_get_authorization_url_with_pkce_forwards_client_id_override + url, _verifier, _state = @um.get_authorization_url_with_pkce( + redirect_uri: "https://app.example.com/cb", + provider: "GoogleOAuth", + client_id: "client_override" + ) + params = URI.decode_www_form(URI.parse(url).query).to_h + assert_equal "client_override", params["client_id"] + end + # H11 def test_authenticate_with_code_pkce_posts_correct_body stub = stub_request(:post, "https://api.workos.com/user_management/authenticate")