From 1df0159f6518345647b54cd4bfcdc8942cb8d6c5 Mon Sep 17 00:00:00 2001 From: Muhsin Keloth Date: Mon, 21 Apr 2025 19:50:08 +0530 Subject: [PATCH] chore: add oauth flow --- .../github/callbacks_controller.rb | 71 +++++++++++++++++++ app/helpers/github/integration_helper.rb | 47 ++++++++++++ app/models/integrations/app.rb | 25 ++++++- config/integration/apps.yml | 2 +- config/routes.rb | 4 ++ 5 files changed, 147 insertions(+), 2 deletions(-) create mode 100644 app/controllers/github/callbacks_controller.rb create mode 100644 app/helpers/github/integration_helper.rb diff --git a/app/controllers/github/callbacks_controller.rb b/app/controllers/github/callbacks_controller.rb new file mode 100644 index 000000000..0bbd9b46c --- /dev/null +++ b/app/controllers/github/callbacks_controller.rb @@ -0,0 +1,71 @@ +class Github::CallbacksController < ApplicationController + include Github::IntegrationHelper + + def show + @response = oauth_client.auth_code.get_token( + params[:code], + redirect_uri: "#{base_url}/github/callback" + ) + + handle_response + rescue StandardError => e + Rails.logger.error("Github callback error: #{e.message}") + redirect_to github_redirect_uri + end + + private + + def oauth_client + app_id = GlobalConfigService.load('GITHUB_CLIENT_ID', nil) + app_secret = GlobalConfigService.load('GITHUB_CLIENT_SECRET', nil) + + OAuth2::Client.new( + app_id, + app_secret, + { + site: 'https://github.com', + token_url: '/login/oauth/access_token', + authorize_url: '/login/oauth/authorize' + } + ) + end + + def handle_response + hook = account.hooks.new( + access_token: parsed_body['access_token'], + status: 'enabled', + app_id: 'github', + settings: { + token_type: parsed_body['token_type'], + scope: parsed_body['scope'] + } + ) + hook.save! + redirect_to github_redirect_uri + rescue StandardError => e + Rails.logger.error("Github callback error: #{e.message}") + redirect_to github_redirect_uri + end + + def account + @account ||= Account.find(account_id) + end + + def account_id + return unless params[:state] + + verify_github_token(params[:state]) + end + + def github_redirect_uri + "#{ENV.fetch('FRONTEND_URL', nil)}/app/accounts/#{account.id}/settings/integrations/github" + end + + def parsed_body + @parsed_body ||= @response.response.parsed + end + + def base_url + ENV.fetch('FRONTEND_URL', 'http://localhost:3000') + end +end diff --git a/app/helpers/github/integration_helper.rb b/app/helpers/github/integration_helper.rb new file mode 100644 index 000000000..ad1cb880c --- /dev/null +++ b/app/helpers/github/integration_helper.rb @@ -0,0 +1,47 @@ +module Github::IntegrationHelper + # Generates a signed JWT token for Github integration + # + # @param account_id [Integer] The account ID to encode in the token + # @return [String, nil] The encoded JWT token or nil if client secret is missing + def generate_github_token(account_id) + return if client_secret.blank? + + JWT.encode(token_payload(account_id), client_secret, 'HS256') + rescue StandardError => e + Rails.logger.error("Failed to generate Github token: #{e.message}") + nil + end + + def token_payload(account_id) + { + sub: account_id, + iat: Time.current.to_i + } + end + + # Verifies and decodes a Github JWT token + # + # @param token [String] The JWT token to verify + # @return [Integer, nil] The account ID from the token or nil if invalid + def verify_github_token(token) + return if token.blank? || client_secret.blank? + + decode_token(token, client_secret) + end + + private + + def client_secret + @client_secret ||= GlobalConfigService.load('GITHUB_CLIENT_SECRET', nil) + end + + def decode_token(token, secret) + JWT.decode(token, secret, true, { + algorithm: 'HS256', + verify_expiration: true + }).first['sub'] + rescue StandardError => e + Rails.logger.error("Unexpected error verifying Github token: #{e.message}") + nil + end +end diff --git a/app/models/integrations/app.rb b/app/models/integrations/app.rb index 080438ffd..d99d9a2ac 100644 --- a/app/models/integrations/app.rb +++ b/app/models/integrations/app.rb @@ -1,5 +1,6 @@ class Integrations::App include Linear::IntegrationHelper + include Github::IntegrationHelper attr_accessor :params def initialize(params) @@ -29,7 +30,12 @@ class Integrations::App # There is no way to get the account_id from the linear callback # so we are using the generate_linear_token method to generate a token and encode it in the state parameter def encode_state - generate_linear_token(Current.account.id) + case params[:id] + when 'linear' + generate_linear_token(Current.account.id) + when 'github' + generate_github_token(Current.account.id) + end end def action @@ -38,6 +44,8 @@ class Integrations::App "#{params[:action]}&client_id=#{ENV.fetch('SLACK_CLIENT_ID', nil)}&redirect_uri=#{self.class.slack_integration_url}" when 'linear' build_linear_action + when 'github' + build_github_action else params[:action] end @@ -70,6 +78,17 @@ class Integrations::App ].join('&') end + def build_github_action + app_id = GlobalConfigService.load('GITHUB_CLIENT_ID', nil) + [ + "#{params[:action]}?response_type=code", + "client_id=#{app_id}", + "redirect_uri=#{self.class.github_integration_url}", + "state=#{encode_state}", + 'scope=repo' + ].join('&') + end + def enabled?(account) case params[:id] when 'webhook' @@ -93,6 +112,10 @@ class Integrations::App "#{ENV.fetch('FRONTEND_URL', nil)}/linear/callback" end + def self.github_integration_url + "#{ENV.fetch('FRONTEND_URL', nil)}/github/callback" + end + class << self def apps Hashie::Mash.new(APPS_CONFIG) diff --git a/config/integration/apps.yml b/config/integration/apps.yml index e44c17c77..637c80dff 100644 --- a/config/integration/apps.yml +++ b/config/integration/apps.yml @@ -190,6 +190,6 @@ github: id: github logo: github.png i18n_key: github - action: /github + action: https://github.com/login/oauth/authorize hook_type: account allow_multiple_hooks: false diff --git a/config/routes.rb b/config/routes.rb index 46199db12..5ffcc4c66 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -479,6 +479,10 @@ Rails.application.routes.draw do resources :delivery_status, only: [:create] end + namespace :github do + get :callback, to: 'callbacks#show' + end + get 'microsoft/callback', to: 'microsoft/callbacks#show' get 'google/callback', to: 'google/callbacks#show' get 'instagram/callback', to: 'instagram/callbacks#show'