# typed: false # frozen_string_literal: true module Setsuzoku module Service module WebService module AuthStrategies # The API OAuth Authentication Interface definition. # Any Plugin that implements this must implement all methods required for OAuth. # # Defines all necessary methods for handling authentication for any authentication strategy. class OAuthStrategy < WebService::AuthStrategy extend T::Sig extend T::Helpers def self.required_instance_methods [] end def self.credential_class Setsuzoku::Service::WebService::Credentials::OAuthCredential end def self.token_valid_for 24.hours end # Any api request headers that this service needs to set. # # @return [Hash] sig { override.returns(T::Hash[Symbol, T.untyped]) } def auth_headers { token: self.credential.token } end # Get a new credential. # Exchanges code for token, refresh_token and expires_on. # # @param args [Hash] the code from the initial auth response { code: 'abcdefg' }. # # @return void sig { override.params(args: T.untyped).void } def new_credential!(**args) # get a token object based on the code retrieved from login get_token!(params(grant_type: 'authorization_code', code: args[:code]), :new_token) end # If the auth credentials are valid for this instance and auth_strategy. # # If the token is invalid we should refresh it. And verify that the credentials are now valid. # Otherwise the credentials are already valid. # # @return [Boolean] true if the auth token is valid for the auth_strategy. sig { override.returns(T::Boolean) } def auth_credential_valid? if token_is_invalid? refresh_expired_token! !token_is_invalid? else true end end private # Determine whether the token is no longer valid. # # @return [Boolean] true if the token is invalid. sig { returns(T::Boolean) } def token_is_invalid? active = self.credential.status != 'disabled' active && !self.credential.expires_on.blank? && !self.credential.refresh_token.blank? && (self.credential.expires_on < refresh_before_expiration_time) end sig { returns(DateTime) } def refresh_before_expiration_time 45.minutes.from_now.to_datetime end # Exchange refresh_token for a new token and expires_on. # # @return [Boolean] true if the credential was refreshed successfully sig { void } def refresh_expired_token! get_token!(params(grant_type: 'refresh_token', refresh_token: self.credential.refresh_token), :refresh_token) end # Exchange code for token, refresh_token and expires_on # # @return void sig { params(code: String, redirect_url: String).void } def custom_token!(code: nil, redirect_url: nil) get_token!(params(grant_type: 'authorization_code', code: code, redirect_uri: redirect_url), :refresh_token) end # Exchange code for a new token via POST request to API token url, # and set token, expiry, and status on the integration # # @param [Hash] body the request body for the token POST request # # @return void sig { params(body: T::Hash[Symbol, String], action: Symbol).void } def get_token!(body, action) success = false request = self.api_strategy.request_class.new(action: action, body: body, without_headers: true) resp = self.api_strategy.call_external_api(request: request, strategy: :auth) attrs = {} if resp.data && resp.data[:access_token] attrs[:status] = 'active' attrs[:token] = resp.data[:access_token] attrs[:refresh_token] = resp.data[:refresh_token] if resp.data.key?(:refresh_token) attrs[:expires_on] = resp.data[:expires_in].to_i.seconds.from_now if resp.data[:expires_in] plugin_attrs = self.plugin.credential_fields_to_update(resp) if self.plugin.respond_to?(:credential_fields_to_update) attrs.merge!(plugin_attrs) else attrs[:status] = 'error' end # Only save Setsuzoku known fields, and any additional attributes the plugin specifies # in credential_fields_to_update. if self.credential.respond_to?(:"update_columns") # we issue an update_columns instead of a save/update_attributes # so as to only mutate these attributes # and not any other fields that may have been set for the credential save = self.credential.update_columns(attrs) self.credential.touch if save && self.credential.respond_to?(:"touch") save && resp.success else resp.success end end # Add some default params to the params hash. # # @param params [Hash] the original params hash. # # @return [Hash] the merged params hash. sig { params(params: T::Hash[T.untyped, T.untyped]).returns(T::Hash[T.untyped, T.untyped]) } def params(params) if params.key?(:redirect_uri) params.merge(client_id: self.credential.client_id, client_secret: self.credential.client_secret) else params.merge(client_id: self.credential.client_id, client_secret: self.credential.client_secret, redirect_uri: self.credential.redirect_url_override.nil? ? self.credential.redirect_url : self.credential.redirect_url_override) end end end end end end end