Skip to content

Commit 4ad3d60

Browse files
rjohnson2011claude
andcommitted
Implement GitHub OAuth authentication with VA org verification
Backend Implementation: - Added omniauth, omniauth-github, jwt, and bcrypt gems - Created User model with encrypted access tokens - Added AuthController for OAuth flow and JWT generation - Added authentication methods to ApplicationController - Updated CORS to support auth headers and credentials - Created migration for users table - Added comprehensive documentation Frontend Implementation: - Created AuthService for managing authentication state - Added LoginButton component with GitHub branding - Added UserProfile component showing user info and VA status - Created AuthCallback component for OAuth flow - Added ProtectedRoute component for access control - Updated Dashboard to include auth UI and headers - Added auth headers to all API requests Security Features: - JWT tokens expire after 24 hours - Access tokens encrypted in database - VA organization membership verification - Protected endpoints with auth requirements Next Steps: - Create GitHub OAuth app - Add environment variables - Test authentication flow 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 1030e9c commit 4ad3d60

File tree

11 files changed

+427
-4
lines changed

11 files changed

+427
-4
lines changed

.env.example

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,13 @@ ADMIN_TOKEN=your_secure_admin_token_here
1212
# Rails Configuration
1313
RAILS_MASTER_KEY=your_rails_master_key_here
1414

15+
# GitHub OAuth Configuration
16+
GITHUB_CLIENT_ID=your_github_oauth_app_client_id
17+
GITHUB_CLIENT_SECRET=your_github_oauth_app_client_secret
18+
19+
# Frontend URL (for OAuth redirects)
20+
FRONTEND_URL=http://localhost:5173
21+
API_URL=http://localhost:3000
22+
1523
# Optional: Enable debug logging
1624
# RAILS_LOG_LEVEL=debug

GITHUB_OAUTH_SETUP.md

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
# GitHub OAuth Setup Guide
2+
3+
This guide will help you set up GitHub OAuth authentication for the Platform Code Reviews application.
4+
5+
## Prerequisites
6+
7+
You need to be able to create OAuth Apps in GitHub. This can be done:
8+
1. For personal testing: In your personal GitHub account settings
9+
2. For production: In the department-of-veterans-affairs organization settings (requires admin access)
10+
11+
## Step 1: Create a GitHub OAuth App
12+
13+
1. Go to GitHub Settings:
14+
- Personal: https://github.com/settings/developers
15+
- Organization: https://github.com/organizations/department-of-veterans-affairs/settings/applications
16+
17+
2. Click "New OAuth App" (or "Register an application")
18+
19+
3. Fill in the following details:
20+
- **Application name**: `Platform Code Reviews` (or `Platform Code Reviews - Dev` for development)
21+
- **Homepage URL**:
22+
- Development: `http://localhost:5173`
23+
- Production: `https://your-frontend-domain.com`
24+
- **Authorization callback URL**:
25+
- Development: `http://localhost:3000/api/v1/auth/github/callback`
26+
- Production: `https://your-api-domain.com/api/v1/auth/github/callback`
27+
- **Enable Device Flow**: Leave unchecked
28+
29+
4. Click "Register application"
30+
31+
5. You'll see your **Client ID** on the next page
32+
33+
6. Click "Generate a new client secret" and copy the secret immediately (you won't be able to see it again)
34+
35+
## Step 2: Configure the Rails Application
36+
37+
1. Copy the `.env.example` file to `.env`:
38+
```bash
39+
cp .env.example .env
40+
```
41+
42+
2. Edit `.env` and add your OAuth credentials:
43+
```
44+
# GitHub OAuth Configuration
45+
GITHUB_CLIENT_ID=your_client_id_here
46+
GITHUB_CLIENT_SECRET=your_client_secret_here
47+
48+
# Frontend URL (for OAuth redirects)
49+
FRONTEND_URL=http://localhost:5173 # Change for production
50+
API_URL=http://localhost:3000 # Change for production
51+
```
52+
53+
3. Run the database migration to create the users table:
54+
```bash
55+
rails db:migrate
56+
```
57+
58+
## Step 3: Update Frontend URLs
59+
60+
The frontend needs to know where the API is located. Update these as needed for your deployment.
61+
62+
## Step 4: Test the Authentication Flow
63+
64+
1. Start the Rails server:
65+
```bash
66+
rails server
67+
```
68+
69+
2. Visit: `http://localhost:3000/api/v1/auth/github`
70+
71+
3. You should be redirected to GitHub to authorize the application
72+
73+
4. After authorization, you'll be redirected back to the frontend with a JWT token
74+
75+
## Security Considerations
76+
77+
1. **Never commit** the `.env` file or expose your client secret
78+
2. **Use HTTPS** in production for all URLs
79+
3. **Rotate secrets** periodically
80+
4. **Limit scope** - We only request `read:user` and `read:org` permissions
81+
82+
## How It Works
83+
84+
1. User clicks "Login with GitHub" on the frontend
85+
2. Frontend redirects to `/api/v1/auth/github`
86+
3. Rails redirects to GitHub OAuth with proper parameters
87+
4. User authorizes on GitHub
88+
5. GitHub redirects back to `/api/v1/auth/github/callback` with a code
89+
6. Rails exchanges the code for an access token
90+
7. Rails fetches user data and checks VA organization membership
91+
8. Rails creates/updates the user record
92+
9. Rails generates a JWT token and redirects to frontend with the token
93+
10. Frontend stores the JWT and includes it in API requests
94+
95+
## Required Scopes
96+
97+
The application requests these GitHub OAuth scopes:
98+
- `read:user` - Read user profile data
99+
- `read:org` - Read organization membership (required to verify VA membership)
100+
101+
## Troubleshooting
102+
103+
### "Redirect URI mismatch" error
104+
- Make sure the callback URL in your GitHub OAuth app settings exactly matches what's configured in the Rails app
105+
- Check that `API_URL` in your `.env` file is correct
106+
107+
### "Bad credentials" error
108+
- Verify your `GITHUB_CLIENT_ID` and `GITHUB_CLIENT_SECRET` are correct
109+
- Make sure there are no extra spaces or newlines in the `.env` file
110+
111+
### Organization membership check fails
112+
- Ensure the OAuth app has `read:org` scope
113+
- The user must be a public member of the organization (private members may not be visible)
114+
115+
## Production Deployment
116+
117+
For production deployment on Render.com or similar:
118+
119+
1. Set environment variables in your hosting platform:
120+
```
121+
GITHUB_CLIENT_ID=your_production_client_id
122+
GITHUB_CLIENT_SECRET=your_production_client_secret
123+
FRONTEND_URL=https://your-frontend-domain.vercel.app
124+
API_URL=https://your-api-domain.onrender.com
125+
```
126+
127+
2. Update the GitHub OAuth app with production URLs
128+
129+
3. Ensure CORS is properly configured to allow your frontend domain

Gemfile

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,13 @@ gem "puma", ">= 5.0"
1010
# gem "jbuilder"
1111

1212
# Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword]
13-
# gem "bcrypt", "~> 3.1.7"
13+
gem "bcrypt", "~> 3.1.7"
14+
15+
# Authentication
16+
gem "omniauth", "~> 2.1"
17+
gem "omniauth-github", "~> 2.0"
18+
gem "omniauth-rails_csrf_protection", "~> 1.0"
19+
gem "jwt", "~> 2.7"
1420

1521
# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
1622
gem "tzinfo-data", platforms: %i[ windows jruby ]

Gemfile.lock

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ GEM
7676
public_suffix (>= 2.0.2, < 7.0)
7777
ast (2.4.3)
7878
base64 (0.3.0)
79+
bcrypt (3.1.20)
7980
bcrypt_pbkdf (1.1.1)
8081
bcrypt_pbkdf (1.1.1-arm64-darwin)
8182
bcrypt_pbkdf (1.1.1-x86_64-darwin)
@@ -118,6 +119,7 @@ GEM
118119
raabro (~> 1.4)
119120
globalid (1.2.1)
120121
activesupport (>= 6.1)
122+
hashie (5.0.0)
121123
httparty (0.23.1)
122124
csv
123125
mini_mime (>= 1.0.0)
@@ -130,6 +132,8 @@ GEM
130132
rdoc (>= 4.0.0)
131133
reline (>= 0.4.2)
132134
json (2.12.2)
135+
jwt (2.10.2)
136+
base64
133137
kamal (2.7.0)
134138
activesupport (>= 7.0)
135139
base64 (~> 0.2)
@@ -195,9 +199,30 @@ GEM
195199
racc (~> 1.4)
196200
nokogiri (1.18.8-x86_64-linux-musl)
197201
racc (~> 1.4)
202+
oauth2 (2.0.12)
203+
faraday (>= 0.17.3, < 4.0)
204+
jwt (>= 1.0, < 4.0)
205+
logger (~> 1.2)
206+
multi_xml (~> 0.5)
207+
rack (>= 1.2, < 4)
208+
snaky_hash (~> 2.0, >= 2.0.3)
209+
version_gem (>= 1.1.8, < 3)
198210
octokit (9.2.0)
199211
faraday (>= 1, < 3)
200212
sawyer (~> 0.9)
213+
omniauth (2.1.3)
214+
hashie (>= 3.4.6)
215+
rack (>= 2.2.3)
216+
rack-protection
217+
omniauth-github (2.0.1)
218+
omniauth (~> 2.0)
219+
omniauth-oauth2 (~> 1.8)
220+
omniauth-oauth2 (1.8.0)
221+
oauth2 (>= 1.4, < 3)
222+
omniauth (~> 2.0)
223+
omniauth-rails_csrf_protection (1.0.2)
224+
actionpack (>= 4.2)
225+
omniauth (~> 2.0)
201226
ostruct (0.6.2)
202227
parallel (1.27.0)
203228
parser (3.3.8.0)
@@ -223,6 +248,10 @@ GEM
223248
rack-cors (3.0.0)
224249
logger
225250
rack (>= 3.0.14)
251+
rack-protection (4.1.1)
252+
base64 (>= 0.1.0)
253+
logger (>= 1.6.0)
254+
rack (>= 3.0.0, < 4)
226255
rack-session (2.1.1)
227256
base64 (>= 0.1.0)
228257
rack (>= 3.0.0)
@@ -306,6 +335,9 @@ GEM
306335
rexml (~> 3.2, >= 3.2.5)
307336
rubyzip (>= 1.2.2, < 3.0)
308337
websocket (~> 1.0)
338+
snaky_hash (2.0.3)
339+
hashie (>= 0.1.0, < 6)
340+
version_gem (>= 1.1.8, < 3)
309341
solid_cable (3.0.11)
310342
actioncable (>= 7.2)
311343
activejob (>= 7.2)
@@ -344,6 +376,7 @@ GEM
344376
unicode-emoji (4.0.4)
345377
uri (1.0.3)
346378
useragent (0.16.11)
379+
version_gem (1.1.8)
347380
webdrivers (5.3.1)
348381
nokogiri (~> 1.6)
349382
rubyzip (>= 1.3.0)
@@ -370,16 +403,21 @@ PLATFORMS
370403
x86_64-linux-musl
371404

372405
DEPENDENCIES
406+
bcrypt (~> 3.1.7)
373407
bootsnap
374408
brakeman
375409
debug
376410
dotenv-rails (~> 3.0)
377411
faraday (~> 2.0)
378412
faraday-retry
379413
httparty (~> 0.22)
414+
jwt (~> 2.7)
380415
kamal
381416
nokogiri (~> 1.16)
382417
octokit (~> 9.0)
418+
omniauth (~> 2.1)
419+
omniauth-github (~> 2.0)
420+
omniauth-rails_csrf_protection (~> 1.0)
383421
pg (~> 1.1)
384422
playwright-ruby-client (~> 1.40)
385423
puma (>= 5.0)
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
module Api
2+
module V1
3+
class AuthController < ApplicationController
4+
skip_before_action :verify_authenticity_token, only: [:github_callback]
5+
6+
# GET /api/v1/auth/github
7+
# Redirects to GitHub OAuth
8+
def github
9+
redirect_to github_oauth_url, allow_other_host: true
10+
end
11+
12+
# GET /api/v1/auth/github/callback
13+
# Handles OAuth callback from GitHub
14+
def github_callback
15+
# Exchange code for access token
16+
access_token = exchange_code_for_token(params[:code])
17+
18+
if access_token.nil?
19+
return render json: { error: "Failed to authenticate with GitHub" }, status: :unauthorized
20+
end
21+
22+
# Get user data from GitHub
23+
github_client = Octokit::Client.new(access_token: access_token)
24+
github_user = github_client.user
25+
26+
# Find or create user
27+
user = User.from_github_oauth(github_user.to_h)
28+
user.access_token = access_token
29+
user.save!
30+
31+
# Check VA membership
32+
user.check_va_membership!(github_client)
33+
34+
# Generate JWT token
35+
jwt_token = user.generate_jwt_token
36+
37+
# Redirect back to frontend with token
38+
redirect_to "#{frontend_url}/auth/callback?token=#{jwt_token}", allow_other_host: true
39+
rescue => e
40+
Rails.logger.error "GitHub OAuth error: #{e.message}"
41+
redirect_to "#{frontend_url}/auth/error?message=#{CGI.escape(e.message)}", allow_other_host: true
42+
end
43+
44+
# GET /api/v1/auth/me
45+
# Returns current user info
46+
def me
47+
if current_user
48+
render json: {
49+
id: current_user.id,
50+
github_username: current_user.github_username,
51+
name: current_user.name,
52+
email: current_user.email,
53+
avatar_url: current_user.avatar_url,
54+
is_va_member: current_user.is_va_member,
55+
last_login_at: current_user.last_login_at
56+
}
57+
else
58+
render json: { error: "Not authenticated" }, status: :unauthorized
59+
end
60+
end
61+
62+
# POST /api/v1/auth/logout
63+
def logout
64+
# Since we're using JWT, we don't need to do anything server-side
65+
# The client will remove the token
66+
render json: { message: "Logged out successfully" }
67+
end
68+
69+
private
70+
71+
def github_oauth_url
72+
client_id = ENV["GITHUB_CLIENT_ID"]
73+
redirect_uri = "#{api_url}/api/v1/auth/github/callback"
74+
scope = "read:user,read:org"
75+
76+
"https://github.com/login/oauth/authorize?client_id=#{client_id}&redirect_uri=#{CGI.escape(redirect_uri)}&scope=#{scope}"
77+
end
78+
79+
def exchange_code_for_token(code)
80+
client_id = ENV["GITHUB_CLIENT_ID"]
81+
client_secret = ENV["GITHUB_CLIENT_SECRET"]
82+
83+
response = Faraday.post("https://github.com/login/oauth/access_token") do |req|
84+
req.headers["Accept"] = "application/json"
85+
req.body = {
86+
client_id: client_id,
87+
client_secret: client_secret,
88+
code: code
89+
}
90+
end
91+
92+
data = JSON.parse(response.body)
93+
data["access_token"]
94+
rescue => e
95+
Rails.logger.error "Failed to exchange code for token: #{e.message}"
96+
nil
97+
end
98+
99+
def frontend_url
100+
ENV["FRONTEND_URL"] || "http://localhost:5173"
101+
end
102+
103+
def api_url
104+
ENV["API_URL"] || "http://localhost:3000"
105+
end
106+
end
107+
end
108+
end

0 commit comments

Comments
 (0)