Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 39 additions & 5 deletions app/controllers/sessions_controller.rb
Original file line number Diff line number Diff line change
@@ -1,14 +1,48 @@
class SessionsController < ApplicationController
def new
redirect_uri = url_for(action: :create, only_path: false)
def hca_new
redirect_uri = url_for(action: :hca_create, only_path: false)

redirect_to User.hca_authorize_url(redirect_uri),
host: "https://auth.hackclub.com",
allow_other_host: "https://auth.hackclub.com"
end

def hca_create
if params[:error].present?
Rails.logger.error "Slack OAuth error: #{params[:error]}"
Sentry.capture_message("Slack OAuth error: #{params[:error]}")
redirect_to root_path, alert: "Failed to authenticate with Slack. Error ID: #{Sentry.last_event_id}"
return
end

redirect_uri = url_for(action: :hca_create, only_path: false)

@user = User.from_hca_token(params[:code], redirect_uri)

# if @user&.persisted?
if @user&.persisted?
session[:user_id] = @user.id

if @user.data_migration_jobs.empty?
MigrateUserFromHackatimeJob.perform_later(@user.id)
end

redirect_to root_path, notice: "Successfully signed in with Slack! Welcome!"
else
redirect_to root_path, alert: "Failed to authenticate with Hack Club Auth!"
end
end

def slack_new
redirect_uri = url_for(action: :slack_create, only_path: false)
Rails.logger.info "Starting Slack OAuth flow with redirect URI: #{redirect_uri}"
redirect_to User.authorize_url(redirect_uri, close_window: params[:close_window].present?, continue_param: params[:continue]),
redirect_to User.slack_authorize_url(redirect_uri, close_window: params[:close_window].present?, continue_param: params[:continue]),
host: "https://slack.com",
allow_other_host: "https://slack.com"
end

def create
redirect_uri = url_for(action: :create, only_path: false)
def slack_create
redirect_uri = url_for(action: :slack_create, only_path: false)

if params[:error].present?
Rails.logger.error "Slack OAuth error: #{params[:error]}"
Expand Down
52 changes: 50 additions & 2 deletions app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ class User < ApplicationRecord

after_create :create_signup_activity
before_validation :normalize_username
encrypts :slack_access_token, :github_access_token
encrypts :slack_access_token, :github_access_token, :hca_access_token

validates :slack_uid, uniqueness: true, allow_nil: true
validates :github_uid, uniqueness: { conditions: -> { where.not(github_access_token: nil) } }, allow_nil: true
Expand Down Expand Up @@ -327,7 +327,18 @@ def update_slack_status
})
end

def self.authorize_url(redirect_uri, close_window: false, continue_param: nil)
def self.hca_authorize_url(redirect_uri)
params = {
redirect_uri:,
client_id: ENV["HCA_CLIENT_ID"],
response_type: "code",
scope: "email"
}

URI.parse("#{HCAService.host}/oauth/authorize?#{params.to_query}")
end

def self.slack_authorize_url(redirect_uri, close_window: false, continue_param: nil)
state = {
token: SecureRandom.hex(24),
close_window: close_window,
Expand Down Expand Up @@ -355,6 +366,43 @@ def self.github_authorize_url(redirect_uri)
URI.parse("https://github.com/login/oauth/authorize?#{params.to_query}")
end

def self.from_hca_token(code, redirect_uri)
# Exchange code for token
response = HTTP.post("#{HCAService.host}/oauth/token", form: {
client_id: ENV["HCA_CLIENT_ID"],
client_secret: ENV["HCA_CLIENT_SECRET"],
redirect_uri: redirect_uri,
code: code,
grant_type: "authorization_code"
})

data = JSON.parse(response.body.to_s)

access_token = data["access_token"]
return nil if access_token.nil?

# get user info
identity = ::HCAService.me(access_token)
# find by HCA ID
@user = User.find_by_hca_id(identity["id"]) unless identity["id"].blank?
# find by slack_id
@user ||= User.find_by_slack_uid(identity["slack_id"]) unless identity["slack_id"].blank?
# find by email
@user ||= begin
EmailAddress.find_by(email: identity["email"])&.user unless identity["email"].blank?
end

# update scopes if user exists
@user.update(hca_scopes: identity["scopes"], hca_id: identity["id"]) if @user

# if no user, create one
@user ||= begin
u = User.create!(hca_id: identity["id"], slack_uid: identity["slack_id"], hca_scopes: identity["scopes"])
EmailAddress.create!(email: identity["email"], user: u) unless identity["email"].blank?
u
end
end
Comment on lines +394 to +404
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: hca_access_token is retrieved but not persisted to the User object in from_hca_token(), causing future HCA API calls to fail.
Severity: CRITICAL | Confidence: High

🔍 Detailed Analysis

The hca_access_token is retrieved from the OAuth response but is never stored on the User object during either update() or create!() calls within the from_hca_token() method. This token is discarded after a single use to fetch user identity, leading to future HCA API calls failing due to a missing token. This behavior is inconsistent with how slack_access_token and github_access_token are persistently stored.

💡 Suggested Fix

Add hca_access_token: access_token to both the update() and create!() calls within the from_hca_token() method to ensure the token is stored.

🤖 Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location: app/models/user.rb#L369-L404

Potential issue: The `hca_access_token` is retrieved from the OAuth response but is
never stored on the `User` object during either `update()` or `create!()` calls within
the `from_hca_token()` method. This token is discarded after a single use to fetch user
identity, leading to future HCA API calls failing due to a missing token. This behavior
is inconsistent with how `slack_access_token` and `github_access_token` are persistently
stored.

Did we get this right? 👍 / 👎 to inform future reviews.
Reference ID: 5281691

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

intentionally left out for now while testing-- these changes aren't shown to the front-end user for now


def self.from_slack_token(code, redirect_uri)
# Exchange code for token
response = HTTP.post("https://slack.com/api/oauth.v2.access", form: {
Expand Down
18 changes: 18 additions & 0 deletions app/services/hca_service.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
module HCAService
def host
if Rails.env.production?
"https://auth.hackclub.com"
else
"https://hca.dinosaurbbq.org"
end
end

def me(user_token)
raise ArgumentError, "user_token is required" unless user_token

response = HTTP.auth("Bearer " + user_token)
.get(host + "/api/v1/me")
JSON.parse(response.body)
end
module_function :me, :host
end
9 changes: 8 additions & 1 deletion app/views/static_pages/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,16 @@
<% else %>
<h1 class="font-bold mt-1 mb-1 text-5xl text-center">Track How Much You <span class="text-primary">Code</span></h1>
<div class="flex flex-col w-full max-w-[50vw] mx-auto mb-22">
<% if params[:hca_auth].present? %>
<%= link_to hca_auth_path, class: "inline-flex items-center justify-center px-6 py-3 rounded text-white font-bold cursor-pointer border-none w-full my-2 bg-primary" do %>
<img src="/images/slack.png" class="h-8 w-8 mr-2 bg-white rounded-full p-1">
<span class="hidden md:flex">Sign in with Hack Club</span>
<% end %>
<% end %>

<%= link_to slack_auth_path, class: "inline-flex items-center justify-center px-6 py-3 rounded text-white font-bold cursor-pointer border-none w-full my-2 bg-primary" do %>
<img src="/images/slack.png" class="h-8 w-8 mr-2 bg-white rounded-full p-1">
<span class="hidden md:flex">Sign in with Hack Club Slack</span>
<span class="hidden md:flex">Sign in with Slack</span>
<% end %>

<div class="flex items-center my-4">
Expand Down
6 changes: 3 additions & 3 deletions config/initializers/inflections.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,6 @@
# end

# These inflection rules are supported but not enabled by default:
# ActiveSupport::Inflector.inflections(:en) do |inflect|
# inflect.acronym "RESTful"
# end
ActiveSupport::Inflector.inflections(:en) do |inflect|
inflect.acronym "HCA"
end
6 changes: 4 additions & 2 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,10 @@ def matches?(request)
get "/what-is-hackatime", to: "static_pages#what_is_hackatime"

# Auth routes
get "/auth/slack", to: "sessions#new", as: :slack_auth
get "/auth/slack/callback", to: "sessions#create"
get "/auth/hca", to: "sessions#hca_new", as: :hca_auth
get "/auth/hca/callback", to: "sessions#hca_create"
get "/auth/slack", to: "sessions#slack_new", as: :slack_auth
get "/auth/slack/callback", to: "sessions#slack_create"
get "/auth/github", to: "sessions#github_new", as: :github_auth
get "/auth/github/callback", to: "sessions#github_create"
delete "/auth/github/unlink", to: "sessions#github_unlink", as: :github_unlink
Expand Down
7 changes: 7 additions & 0 deletions db/migrate/20251202221230_add_hca_fields_to_users.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
class AddHCAFieldsToUsers < ActiveRecord::Migration[8.1]
def change
add_column :users, :hca_id, :string
add_column :users, :hca_scopes, :string, array: true, default: []
add_column :users, :hca_access_token, :string
end
end
5 changes: 4 additions & 1 deletion db/schema.rb

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.