Skip to content
Open
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
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ CANVAS_APP_KEY=
CANVAS_CLIENT_ID=
CANVAS_REDIRECT_URI='https://flextensions.eecs.cloud'
CANVAS_URL='https://ucberkeleysandbox.instructure.com'
# Google OAuth Configuration
# Login with Google is only enabled when both variables are set.
# Existing users only — Google login will not create new accounts.
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
## Gradescope Configuration
# We use a single username/password for Gradescope
# This should be a "service account" that can be used to set course settings
Expand Down
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ gem 'ostruct'
# used to authenticate with the LMS
gem 'omniauth'
gem 'omniauth-canvas'
gem 'omniauth-google-oauth2'
gem 'omniauth-oauth2'

# Audit for potentially unsafe database migrations
Expand Down
6 changes: 6 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,11 @@ GEM
omniauth-canvas (2.0.0)
omniauth (~> 2.0)
omniauth-oauth2 (~> 1.8)
omniauth-google-oauth2 (1.2.2)
jwt (>= 2.9.2)
oauth2 (~> 2.0)
omniauth (~> 2.0)
omniauth-oauth2 (~> 1.8)
omniauth-oauth2 (1.9.0)
oauth2 (>= 2.0.2, < 3)
omniauth (~> 2.0)
Expand Down Expand Up @@ -644,6 +649,7 @@ DEPENDENCIES
lograge
omniauth
omniauth-canvas
omniauth-google-oauth2
omniauth-oauth2
ostruct
pg
Expand Down
24 changes: 24 additions & 0 deletions app/controllers/session_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ def omniauth_callback
return
end

if auth.provider == 'google_oauth2'
handle_google_login(auth)
return
end

user_data = {
'id' => auth.uid,
'name' => auth.info.name,
Expand Down Expand Up @@ -89,6 +94,25 @@ def destroy

private

# Google OAuth only authenticates users who already have an account.
# No new accounts are created via Google login.
def handle_google_login(auth)
email = auth.info.email
user = User.find_by(email: email) if email.present?

unless user
Rails.logger.warn("Google login rejected: no user with email #{email.inspect}")
redirect_to root_path,
alert: 'No account found for that Google email. Please log in with Canvas first.'
return
end

session[:username] = user.name
session[:user_id] = user.canvas_uid

redirect_to courses_path, notice: "Logged in! Welcome, #{user.name}!"
end

def ensure_developer_test_enrollments(user)
# Find the test course
test_course = Course.find_by(course_code: 'DEV101')
Expand Down
4 changes: 4 additions & 0 deletions app/helpers/application_helper.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
module ApplicationHelper
def google_oauth_enabled?
ENV['GOOGLE_CLIENT_ID'].present? && ENV['GOOGLE_CLIENT_SECRET'].present?
end

def assignment_link_for(assignment, course)
case assignment.course_to_lms.lms_id
when 1
Expand Down
3 changes: 3 additions & 0 deletions app/views/home/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@
</div>
<div class="col-12 text-center">
<%= link_to 'Login', '/auth/canvas', class: 'btn btn-primary', id: 'login-button-index' %>
<% if google_oauth_enabled? %>
<%= link_to 'Login with Google', '/auth/google_oauth2', class: 'btn btn-outline-primary ms-2', id: 'google-login-button' %>
<% end %>
<% if Rails.env.development? %>
<%= link_to 'Developer Login', '/auth/developer', class: 'btn btn-secondary ms-2', id: 'dev-login-button' %>
<% end %>
Expand Down
8 changes: 8 additions & 0 deletions config/initializers/omniauth.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@ def build_access_token
authorize_url: "/login/oauth2/auth?scope=#{encoded_scopes}"
},
redirect_uri: "#{ENV['CANVAS_REDIRECT_URI']}/auth/canvas/callback"

if ENV['GOOGLE_CLIENT_ID'].present? && ENV['GOOGLE_CLIENT_SECRET'].present?
provider :google_oauth2,
ENV['GOOGLE_CLIENT_ID'],
ENV['GOOGLE_CLIENT_SECRET'],
scope: 'email,profile',
prompt: 'select_account'
end
end

# OmniAuth.config.before_request_phase do |env|
Expand Down
37 changes: 37 additions & 0 deletions spec/Helpers/application_helper_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
require 'rails_helper'

RSpec.describe ApplicationHelper, type: :helper do
describe '#google_oauth_enabled?' do
around do |example|
original_id = ENV['GOOGLE_CLIENT_ID']
original_secret = ENV['GOOGLE_CLIENT_SECRET']
example.run
ENV['GOOGLE_CLIENT_ID'] = original_id
ENV['GOOGLE_CLIENT_SECRET'] = original_secret
end

it 'returns true when both Google OAuth env vars are set' do
ENV['GOOGLE_CLIENT_ID'] = 'fake-id'
ENV['GOOGLE_CLIENT_SECRET'] = 'fake-secret'
expect(helper.google_oauth_enabled?).to be(true)
end

it 'returns false when GOOGLE_CLIENT_ID is missing' do
ENV['GOOGLE_CLIENT_ID'] = nil
ENV['GOOGLE_CLIENT_SECRET'] = 'fake-secret'
expect(helper.google_oauth_enabled?).to be(false)
end

it 'returns false when GOOGLE_CLIENT_SECRET is missing' do
ENV['GOOGLE_CLIENT_ID'] = 'fake-id'
ENV['GOOGLE_CLIENT_SECRET'] = nil
expect(helper.google_oauth_enabled?).to be(false)
end

it 'returns false when both env vars are blank' do
ENV['GOOGLE_CLIENT_ID'] = ''
ENV['GOOGLE_CLIENT_SECRET'] = ''
expect(helper.google_oauth_enabled?).to be(false)
end
end
end
27 changes: 27 additions & 0 deletions spec/controllers/home_controller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,32 @@
expect(response).to redirect_to(courses_path)
end
end

context 'Google login button visibility' do
render_views

around do |example|
original_id = ENV['GOOGLE_CLIENT_ID']
original_secret = ENV['GOOGLE_CLIENT_SECRET']
example.run
ENV['GOOGLE_CLIENT_ID'] = original_id
ENV['GOOGLE_CLIENT_SECRET'] = original_secret
end

it 'shows the Login with Google button when Google OAuth is configured' do
ENV['GOOGLE_CLIENT_ID'] = 'fake-id'
ENV['GOOGLE_CLIENT_SECRET'] = 'fake-secret'
get :index
expect(response.body).to include('Login with Google')
expect(response.body).to include('/auth/google_oauth2')
end

it 'hides the Login with Google button when env vars are not set' do
ENV['GOOGLE_CLIENT_ID'] = nil
ENV['GOOGLE_CLIENT_SECRET'] = nil
get :index
expect(response.body).not_to include('Login with Google')
end
end
end
end
71 changes: 71 additions & 0 deletions spec/controllers/session_controller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,77 @@
end
end

describe 'GET #omniauth_callback (google_oauth2 provider)' do
let(:google_auth_hash) do
OmniAuth::AuthHash.new(
provider: 'google_oauth2',
uid: 'google-uid-1',
info: OpenStruct.new(name: 'Google User', email: 'existing@example.com'),
credentials: {
token: 'google-token',
refresh_token: 'google-refresh',
expires_at: 1.hour.from_now.to_i
}
)
end

before do
request.env['omniauth.auth'] = google_auth_hash
end

context 'when a user with the given email already exists' do
let!(:existing_user) do
User.create!(email: 'existing@example.com', canvas_uid: 'canvas-99', name: 'Existing User')
end

it 'logs the user in and redirects to courses' do
get :omniauth_callback, params: { provider: 'google_oauth2' }

expect(session[:user_id]).to eq('canvas-99')
expect(session[:username]).to eq('Existing User')
expect(response).to redirect_to(courses_path)
expect(flash[:notice]).to include('Logged in!')
end

it 'does not create a new user' do
expect do
get :omniauth_callback, params: { provider: 'google_oauth2' }
end.not_to change(User, :count)
end

it 'does not create LMS credentials for the user' do
expect do
get :omniauth_callback, params: { provider: 'google_oauth2' }
end.not_to change { existing_user.reload.lms_credentials.count }
end
end

context 'when no user with the given email exists' do
it 'does not create a user and redirects to root with an alert' do
expect do
get :omniauth_callback, params: { provider: 'google_oauth2' }
end.not_to change(User, :count)

expect(response).to redirect_to(root_path)
expect(flash[:alert]).to include('No account found')
expect(session[:user_id]).to be_nil
end
end

context 'when the auth hash has no email' do
before do
google_auth_hash.info.email = nil
end

it 'rejects the login with an alert' do
get :omniauth_callback, params: { provider: 'google_oauth2' }

expect(response).to redirect_to(root_path)
expect(flash[:alert]).to include('No account found')
end
end
end

describe 'GET #logout' do
before do
session[:user_id] = 'test_user_id'
Expand Down
Loading