# 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.respond_to?(:"status") ? self.credential.status != 'disabled' : true 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) if resp.data && resp.data[:access_token] self.credential.status = 'active' if self.credential.respond_to?(:"status=") self.credential.token = resp.data[:access_token] self.credential.refresh_token = resp.data[:refresh_token] if resp.data.key?(:refresh_token) self.credential.expires_on = if resp.data[:expires_in] resp.data[:expires_in].to_i.seconds.from_now # elsif resp.data[:issued_at] && self.class.token_valid_for # Time.at(resp.data[:issued_at].to_i / 1000) + self.class.token_valid_for end self.plugin.set_credential_fields!(resp) if self.plugin.respond_to?(:set_credential_fields!) #TODO: figure out a better way to do this when we add other integrations... # # if self.is_a?(FacebookIntegration) # resp = self.exchange_for_long_lived_token # if resp && resp.data['access_token'] # self.token = resp.data['access_token'] # self.refresh_token = resp.data['access_token'] # self.expires_on = 59.days.from_now # end # end else self.credential.status = 'error' if self.credential.respond_to?(:"status=") end if self.credential.respond_to?(:"save") self.credential.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