Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GT-1839, implement facebook auth #1228

Merged
merged 51 commits into from
Mar 30, 2023
Merged
Show file tree
Hide file tree
Changes from 46 commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
67823be
implement facebook auth
andrewroth Mar 8, 2023
9832d44
standardrb fixes, rack update
andrewroth Mar 8, 2023
a492c19
match users based on facebook user id
andrewroth Mar 9, 2023
d36f9c5
remove rescue for bad request, it's not an exception but a reponse type
andrewroth Mar 9, 2023
9b95d15
move facebook code to service to match okta code style
andrewroth Mar 9, 2023
9385ce1
standardrb fixes
andrewroth Mar 9, 2023
5b7f6a4
handle json parse error instead of jwt decode which never happens in …
andrewroth Mar 9, 2023
2b515c5
add test for json error
andrewroth Mar 9, 2023
4b67af7
remove useless assignment variable
andrewroth Mar 9, 2023
c25b937
implement facebook user delete
andrewroth Mar 10, 2023
039f596
standardrb fixes
andrewroth Mar 10, 2023
3d9d46f
switch to account prefix url
andrewroth Mar 10, 2023
45a31f1
track gr_master_person_id from okta
andrewroth Mar 13, 2023
60f691b
use proper query escaping
andrewroth Mar 15, 2023
5685516
security upgrades
andrewroth Mar 15, 2023
1f868c7
Merge branch 'master' into GT-1839-support-facebook-logins
andrewroth Mar 15, 2023
a330241
Update app/controllers/deletion_requests_controller.rb
andrewroth Mar 16, 2023
843cf69
Update app/services/facebook.rb
andrewroth Mar 16, 2023
7131cdb
various tweaks as per code review
andrewroth Mar 16, 2023
07aa6c1
add google auth
andrewroth Mar 16, 2023
630a73f
style fix
andrewroth Mar 16, 2023
fb2fed1
implement apple token verify
andrewroth Mar 20, 2023
5f766c4
Reference the PR that needs to be released for apple_auth
frett Mar 20, 2023
bc06ad5
use the validation from the apple library for the token issuer
andrewroth Mar 20, 2023
0d00606
Merge branch 'GT-1839-support-facebook-logins' of github.com:CruGloba…
andrewroth Mar 20, 2023
6c4a50c
refactor auth services to a base for common patterns
andrewroth Mar 21, 2023
9d744fd
standardrb fixes
andrewroth Mar 21, 2023
3ef394a
implement GT-1871, tracking name and using given_name & family_name
andrewroth Mar 21, 2023
00b1bf3
standardrb fixes
andrewroth Mar 21, 2023
7a69e27
Merge branch 'master' into GT-1839-support-facebook-logins
andrewroth Mar 21, 2023
979619d
fix validate line and fix test failing because of expired token
andrewroth Mar 22, 2023
c7beec2
style fixes
andrewroth Mar 22, 2023
b5f54be
rename apple and google user identifiers to id_token
andrewroth Mar 23, 2023
0c822e3
rename some methods to reflect token from google and apple is id_token
andrewroth Mar 23, 2023
ea4b609
handle apple and google passing in *_id_token instead of access_token
andrewroth Mar 23, 2023
102f39b
rename auth service classes to be consistent
andrewroth Mar 23, 2023
e08df12
style fix
andrewroth Mar 23, 2023
003a14c
fix tests
andrewroth Mar 23, 2023
1b4ab2f
try to incrase coverage; don't need interface error messages now
andrewroth Mar 23, 2023
4137c79
trying to get coverage up
andrewroth Mar 23, 2023
4eb63a7
JWT::ExpiredSignature is a JWT::DecodeError, so it's redundant to cap…
andrewroth Mar 23, 2023
d41b11b
styling fixes
andrewroth Mar 23, 2023
ab42ea8
update schema
andrewroth Mar 27, 2023
d987a99
pull body from response
andrewroth Mar 28, 2023
1ffdcb9
Merge branch 'GT-1839-support-facebook-logins' of github.com:CruGloba…
andrewroth Mar 28, 2023
2ba0255
remove email unique requirement
andrewroth Mar 28, 2023
5e2052f
Update app/services/okta_auth_service.rb
andrewroth Mar 29, 2023
bbbd3a2
tweak okta_auth_service to be more consistent on using primary_key
andrewroth Mar 29, 2023
51f0667
don't need google service_name, now with the new class default is ok
andrewroth Mar 29, 2023
ae392dc
rename AuthServiceBase to BaseAuthService
andrewroth Mar 29, 2023
f77477e
add back the raise stubs for implementing classes
andrewroth Mar 29, 2023
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
7 changes: 7 additions & 0 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,10 @@ ADOBE_CAMPAIGN_SIGNED_JWT=asdf
OKTA_SERVER_URL=https://dev1-signon.okta.com
OKTA_SERVER_PATH=https://dev1-signon.okta.com
OKTA_SERVER_AUDIENCE=https://dev1-signon.okta.com

FACEBOOK_APP_ID=facebook_app_id
FACEBOOK_APP_SECRET=facebook_app_secret

GOOGLE_APP_ID=id.apps.googleusercontent.com

APPLE_CLIENT_ID=org.cru.godtools
5 changes: 5 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,11 @@ gem "rest-client", "~> 2.1.0"
gem "rollbar"
gem "rubyzip", ">= 1.2.2"
gem "validates_email_format_of"
gem "googleauth"

# apple_auth has JWT verification code only in github, not in the latest gem. We should switch to the next gem release when it happens
# relevant PR: https://github.com/rootstrap/apple_auth/pull/29
gem "apple_auth", github: "rootstrap/apple_auth", ref: "d71d10d370f2ec2107e361a8ef725e037ad2e152"

group :development, :test do
gem "action-cable-testing"
Expand Down
29 changes: 23 additions & 6 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
GIT
remote: https://github.com/rootstrap/apple_auth.git
revision: d71d10d370f2ec2107e361a8ef725e037ad2e152
ref: d71d10d370f2ec2107e361a8ef725e037ad2e152
specs:
apple_auth (1.0.0)
jwt (~> 2.2)
oauth2 (~> 1.4)

GEM
remote: https://rubygems.org/
specs:
Expand Down Expand Up @@ -118,7 +127,7 @@ GEM
case_transform (0.2)
activesupport
coderay (1.1.3)
concurrent-ruby (1.1.10)
concurrent-ruby (1.2.2)
crack (0.4.5)
rexml
crass (1.0.6)
Expand Down Expand Up @@ -159,7 +168,7 @@ GEM
mime-types (>= 1.0)
formatador (0.3.0)
gems (1.2.0)
globalid (1.0.1)
globalid (1.1.0)
activesupport (>= 5.0)
google-api-client (0.53.0)
google-apis-core (~> 0.1)
Expand Down Expand Up @@ -256,7 +265,7 @@ GEM
mini_magick (4.11.0)
mini_mime (1.1.2)
mini_portile2 (2.8.1)
minitest (5.17.0)
minitest (5.18.0)
msgpack (1.6.1)
multi_json (1.15.0)
multi_xml (0.6.0)
Expand All @@ -282,6 +291,12 @@ GEM
notiffany (0.1.3)
nenv (~> 0.1)
shellany (~> 0.0)
oauth2 (1.4.11)
faraday (>= 0.17.3, < 3.0)
jwt (>= 1.0, < 3.0)
multi_json (~> 1.3)
multi_xml (~> 0.5)
rack (>= 1.2, < 4)
oj (3.14.2)
os (1.1.1)
ougai (2.0.0)
Expand All @@ -307,7 +322,7 @@ GEM
rack (>= 1.2.0)
rack-protection (2.2.3)
rack
rack-test (2.0.2)
rack-test (2.1.0)
rack (>= 1.3)
raddocs (2.2.0)
haml (>= 4.0)
Expand All @@ -331,7 +346,7 @@ GEM
rails-dom-testing (2.0.3)
activesupport (>= 4.2.0)
nokogiri (>= 1.6)
rails-html-sanitizer (1.4.4)
rails-html-sanitizer (1.5.0)
loofah (~> 2.19, >= 2.19.1)
railties (6.1.7.3)
actionpack (= 6.1.7.3)
Expand Down Expand Up @@ -491,6 +506,7 @@ DEPENDENCIES
active_storage_validations
adobe-campaign (~> 0.4.1)
amazing_print
apple_auth!
aws-sdk-s3
bootsnap (>= 1.4.4)
brakeman
Expand All @@ -504,6 +520,7 @@ DEPENDENCIES
factory_bot_rails
file_validators
google-api-client (~> 0.53)
googleauth
guard-rspec
guard-rubocop
httparty
Expand Down Expand Up @@ -544,4 +561,4 @@ RUBY VERSION
ruby 3.0.5p211

BUNDLED WITH
2.3.11
2.3.12
42 changes: 25 additions & 17 deletions app/controllers/auth_controller.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,33 @@
# frozen_string_literal: true

class AuthController < ApplicationController
THIRD_PARTY_AUTH_METHODS = [:okta, :facebook, :google, :apple]

def create
token = data_attrs[:okta_access_token] ? auth_with_okta : auth_with_code
method = THIRD_PARTY_AUTH_METHODS.detect { |method| data_attrs[:"#{method}_access_token"] || data_attrs[:"#{method}_id_token"] }
token = case method
when :apple
# special case for apple, which has given and family name passed in
user = AppleAuthService.find_user_by_token(data_attrs[:apple_id_token], data_attrs[:apple_given_name], data_attrs[:apple_family_name])
AuthToken.new(user: user)
when :google
user = GoogleAuthService.find_user_by_token(data_attrs[:google_id_token])
AuthToken.new(user: user)
when :okta, :facebook
user = "::#{method.to_s.capitalize}AuthService".constantize.find_user_by_token(data_attrs[:"#{method}_access_token"])
AuthToken.new(user: user)
else
AccessCode.validate(data_attrs[:code])
AuthToken.new
Comment on lines +19 to +21
Copy link
Contributor

Choose a reason for hiding this comment

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

No real action point here, but just letting you know that the AccessCode logic has been slated to be removed from mobile-content-api since November 2021. See GT-1348

end

render json: token, status: :created if token
rescue AuthServiceBase::FailedAuthentication => e
render_bad_request e.message
nil
rescue AccessCode::FailedAuthentication => e
render_bad_request e.message
nil
end

private
Expand All @@ -14,20 +38,4 @@ def render_bad_request(message)

render_error(code, :bad_request)
end

def auth_with_code
AccessCode.validate(data_attrs[:code])
AuthToken.new
rescue AccessCode::FailedAuthentication => e
render_bad_request e.message
nil
end

def auth_with_okta
user = Okta.find_user_by_access_token(data_attrs[:okta_access_token])
AuthToken.new(user: user)
rescue Okta::FailedAuthentication => e
render_bad_request e.message
nil
end
end
25 changes: 25 additions & 0 deletions app/controllers/deletion_requests_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
class DeletionRequestsController < ApplicationController
# disable CSRF protection, as it doesn't make sense in this case
protect_from_forgery with: :null_session

def facebook
begin
dr = DeletionRequest.from_signed_fb(params["signed_request"])
rescue DeletionRequest::FailedAuthentication => e
render json: {"error" => e.to_s}
return
end

dr.run

render json: {
url: deletion_request_url(dr.pid),
confirmation_code: dr.pid
}
end

def show
dr = DeletionRequest.find_by!(pid: params[:id])
render json: {"data" => dr.deleted? ? "Your data has been completely deleted" : "Your deletion request is still in progress"}
end
end
2 changes: 0 additions & 2 deletions app/controllers/users_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ def show
end

def destroy
@user.user_counters.destroy_all
@user.favorite_tools.destroy_all
@user.destroy!
render json: "", status: 204
end
Expand Down
47 changes: 47 additions & 0 deletions app/models/deletion_request.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
class DeletionRequest < ApplicationRecord
validates_presence_of :uid, :provider, :pid

# there can only be one entry with given provider + uid
validates_uniqueness_of :uid, scope: :provider

before_validation :set_pid

def run
associated_user&.destroy!
end

def deleted?
associated_user.nil?
end

def self.from_signed_fb(req)
encoded, payload = req.split(".", 2)
decoded = Base64.urlsafe_decode64(encoded)
data = JSON.parse(Base64.urlsafe_decode64(payload))

# we need to verify the digest is the same
exp = OpenSSL::HMAC.digest("SHA256", ENV["FACEBOOK_APP_SECRET"], payload)
raise FailedAuthentication, "FB deletion callback called with invalid data" if decoded != exp

return unless data
DeletionRequest.create(provider: "facebook", uid: data["user_id"])
end

private

def associated_user
case provider
when "facebook"
User.find_by(facebook_user_id: uid)
end
end

def set_pid
if pid.blank?
self.pid = SecureRandom.hex(4)
end
end

class FailedAuthentication < StandardError
end
end
11 changes: 4 additions & 7 deletions app/models/user.rb
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
class User < ApplicationRecord
has_many :user_counters
has_many :favorite_tools
has_many :user_counters, dependent: :destroy
has_many :favorite_tools, dependent: :destroy
has_many :tools, through: :favorite_tools

validates :sso_guid, uniqueness: true, presence: true

# while the email needs to be validated case-insensitively, we'll
# let Rails pass the insensitive check down to postgres's citext type
validates :email, uniqueness: {case_sensitive: true}, presence: true
validates :sso_guid, uniqueness: true, presence: true, unless: -> { facebook_user_id.present? || google_user_id.present? || apple_user_id.present? }
validates :email, presence: true
end
10 changes: 9 additions & 1 deletion app/serializers/user_serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,18 @@

class UserSerializer < ActiveModel::Serializer
type "user"
attributes :sso_guid, :created_at
attributes :sso_guid, :created_at, :given_name, :family_name, :name
has_many :tools, key: "favorite-tools", serializer: ResourceFavoritedSerializer

def created_at
object.created_at.iso8601 # without this, the default serializer datetime will add 3 ms digits which we prefer not to have
end

def given_name
object.first_name
end

def family_name
object.last_name
end
end
52 changes: 52 additions & 0 deletions app/services/apple_auth_service.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# frozen_string_literal: true

class AppleAuthService < AuthServiceBase
class << self
def find_user_by_token(apple_id_token, apple_given_name = nil, apple_family_name = nil)
decoded_token = decode_token(apple_id_token)
validate_token!(apple_id_token, decoded_token)
validate_expected_fields!(decoded_token)

apple_id_token = remote_user_id(decoded_token)
user_atts = extract_user_atts(apple_id_token, decoded_token, apple_id_token)
user_atts["first_name"] = apple_given_name if apple_given_name.present?
user_atts["last_name"] = apple_family_name if apple_family_name.present?
setup_user(apple_id_token, user_atts)
rescue JSON::ParserError => e
raise FailedAuthentication, e.message
rescue JWT::DecodeError => e
raise FailedAuthentication, e.message
rescue AppleAuth::Conditions::JWTValidationError => e
raise FailedAuthentication, e.message
end

private

def expected_fields
%w[sub email iss aud]
end

def decode_token(apple_id_token)
AppleAuth::JWTDecoder.new(apple_id_token).call
end

def validate_token!(apple_id_token, decoded_token)
raise FailedAuthentication, "Sub is missing from payload" unless decoded_token["sub"]
AppleAuth::UserIdentity.new(decoded_token["sub"], apple_id_token).validate!
frett marked this conversation as resolved.
Show resolved Hide resolved
end

def remote_user_id(decoded_token)
decoded_token["sub"]
end

def extract_user_atts(_apple_id_token, decoded_token, remote_user_id)
{
apple_user_id: remote_user_id,
email: decoded_token["email"]
}.with_indifferent_access
end
end

class FailedAuthentication < AuthServiceBase::FailedAuthentication
end
end
45 changes: 45 additions & 0 deletions app/services/auth_service_base.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# frozen_string_literal: true

class AuthServiceBase
knutsenm marked this conversation as resolved.
Show resolved Hide resolved
include HTTParty

class << self
def find_user_by_token(access_token)
decoded_token = decode_token(access_token)
knutsenm marked this conversation as resolved.
Show resolved Hide resolved
validate_token!(access_token, decoded_token)
validate_expected_fields!(decoded_token)

user_atts = extract_user_atts(access_token, decoded_token)
setup_user(remote_user_id(decoded_token), user_atts)
rescue JSON::ParserError => e
raise self::FailedAuthentication, e.message
rescue JWT::DecodeError => e
raise self::FailedAuthentication, e.message
end

private

def validate_expected_fields!(decoded_token)
unless decoded_token.present? && decoded_token.is_a?(Hash) && decoded_token.keys.to_set.superset?(expected_fields.to_set)
raise FailedAuthentication, "Error validating #{service_name} access_token: Missing some or all user fields (got #{decoded_token.keys.join(", ")}, expected #{expected_fields.join(", ")})"
end
end

def setup_user(remote_user_id, user_atts)
user = User.where(primary_key => remote_user_id).first_or_initialize
user.update!(user_atts)
user
end

def service_name
name.gsub("AuthService", "").downcase
end

def primary_key
:"#{service_name}_user_id"
end
end

class FailedAuthentication < StandardError
end
end
Loading