Skip to content

Instantly share code, notes, and snippets.

@fractaledmind
Last active April 24, 2025 14:31
Show Gist options
  • Save fractaledmind/3472af8c8cf257dec6bd082e260494eb to your computer and use it in GitHub Desktop.
Save fractaledmind/3472af8c8cf257dec6bd082e260494eb to your computer and use it in GitHub Desktop.
GitHub OAuth flow for Rails applications without any dependencies
# app/controllers/github/authorizations_controller.rb
module GitHub
class AuthorizationsController < ApplicationController
allow_unauthenticated_access only: :create
before_action :resume_session, only: :show
rescue_from ActionController::InvalidAuthenticityToken do
redirect_to root_path, alert: "Authentication with GitHub failed: invalid state token"
end
rescue_from ActionController::ParameterMissing do
redirect_to root_path, alert: "Authentication with GitHub failed: invalid code token"
end
# see: https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps
AUTHORIZE_URI = URI("https://github.com/login/oauth/authorize")
AUTHORIZE_SCOPES = "read:user user:email"
TOKEN_URI = URI("https://github.com/login/oauth/access_token")
USER_INFO_URI = URI("https://api.github.com/user")
CLIENT_ID = Rails.application.credentials.github.oauth.client_id
CLIENT_SECRET = Rails.application.credentials.github.oauth.client_secret
# POST /github/authorization
def create
store_origin
store_destination
redirect_to authorize_url, allow_other_host: true
end
# GET /github/authorization?code=String&state=String
def show
validate_state_token!
details = gather_authorization_details!
user_attributes = {
avatar_url: details.dig("info", "avatar_url"),
github_username: details.dig("info", "login"),
twitter_username: details.dig("info", "twitter_username")
}
if (user = User.find_by(github_uid: details["uid"])).present?
user.update(user_attributes)
sign_in(user:)
redirect_to after_oauth_path,
notice: "Successfully updated GitHub account"
elsif Current.user.present?
Current.user.update(
github_uid: details["uid"],
**user_attributes
)
redirect_to after_oauth_path,
notice: "Successfully connected GitHub account"
else
user = User.new(
github_uid: details["uid"],
**user_attributes
)
if user.save
sign_in(user:)
redirect_to after_oauth_path,
notice: "Signed in successfully via GitHub"
else
redirect_to new_session_path,
alert: "Authentication with GitHub failed: invalid user details"
end
end
rescue ApplicationClient::Error => error
error_response = JSON.parse(error.message, symbolize_names: true)
redirect_back fallback_location: new_session_path,
alert: error_response[:message]
end
private
def store_origin
return unless params.key?(:origin)
session["oauth.origin"] = params[:origin]
end
def store_destination
return unless params.key?(:destination)
session["oauth.destination"] = params[:destination]
end
def authorize_url
AUTHORIZE_URL.tap do |uri|
uri.query = Rack::Utils.build_query({
client_id: CLIENT_ID,
redirect_uri: callback_url,
response_type: "code",
scope: AUTHORIZE_SCOPES,
state: form_authenticity_token # prevent CSRF
})
end.to_s
end
def validate_state_token!
state_token = params.fetch(:state, nil)
unless valid_authenticity_token?(session, state_token)
raise ActionController::InvalidAuthenticityToken, "The state=#{state_token} token is inauthentic."
end
end
def gather_authorization_details!
access_credentials = request_access_credentials!
user_info = request_user_info!(access_token: access_credentials.access_token)
{
"provider" => :github,
"uid" => user_info.id,
"credentials" => access_credentials.parsed_body.as_json["table"],
"info" => user_info.parsed_body.as_json["table"]
}
end
def request_access_credentials!
client = ApplicationClient.new
client.post(TOKEN_URI, body: {
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
code: params.fetch(:code),
grant_type: "authorization_code",
redirect_uri: callback_url
})
end
def request_user_info!(access_token:)
client = ApplicationClient.new(token: access_token)
client.get(USER_INFO_URI)
end
def callback_url
url_for(action: :show, only_path: false)
end
def after_oauth_path
session.delete("oauth.destination") || user_root_path
end
end
end
# config/routes.rb
namespace :github do
resource :authorization, only: %i[ create show ]
end
<%= button_to "Sign in with GitHub",
github_authorization_path,
data: { turbo: false } %>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment