discourse/lib/auth/google_oauth2_authenticator.rb
David Taylor e0a6d12c55
Use service account credentials for fetching google hd groups (#18329)
The previous implementation would attempt to fetch groups using the end-user's Google auth token. This only worked for admin accounts, or users with 'delegated' access to the `admin.directory.group.readonly` API.

This commit changes the approach to use a single 'service account' for fetching the groups. This removes the need to add permissions to all regular user accounts. I'll be updating the [meta docs](https://meta.discourse.org/t/226850) with instructions on setting up the service account.

This is technically a breaking change in behavior, but the existing implementation was marked experimental, and is currently unusable in production google workspace environments.
2022-10-13 16:04:42 +01:00

136 lines
4.1 KiB
Ruby

# frozen_string_literal: true
class Auth::GoogleOAuth2Authenticator < Auth::ManagedAuthenticator
GROUPS_SCOPE ||= "https://www.googleapis.com/auth/admin.directory.group.readonly"
GROUPS_DOMAIN ||= "admin.googleapis.com"
GROUPS_PATH ||= "/admin/directory/v1/groups"
OAUTH2_BASE_URL ||= "https://oauth2.googleapis.com"
def name
"google_oauth2"
end
def enabled?
SiteSetting.enable_google_oauth2_logins
end
def primary_email_verified?(auth_token)
# note, emails that come back from google via omniauth are always valid
# this protects against future regressions
auth_token[:extra][:raw_info][:email_verified]
end
def register_middleware(omniauth)
options = {
setup: lambda { |env|
strategy = env["omniauth.strategy"]
strategy.options[:client_id] = SiteSetting.google_oauth2_client_id
strategy.options[:client_secret] = SiteSetting.google_oauth2_client_secret
if (google_oauth2_hd = SiteSetting.google_oauth2_hd).present?
strategy.options[:hd] = google_oauth2_hd
end
if (google_oauth2_prompt = SiteSetting.google_oauth2_prompt).present?
strategy.options[:prompt] = google_oauth2_prompt.gsub("|", " ")
end
# All the data we need for the `info` and `credentials` auth hash
# are obtained via the user info API, not the JWT. Using and verifying
# the JWT can fail due to clock skew, so let's skip it completely.
# https://github.com/zquestz/omniauth-google-oauth2/pull/392
strategy.options[:skip_jwt] = true
}
}
omniauth.provider :google_oauth2, options
end
def after_authenticate(auth_token, existing_account: nil)
groups = provides_groups? ? raw_groups(auth_token.uid) : nil
if groups
auth_token.extra[:raw_groups] = groups
end
result = super
if groups
result.associated_groups = groups.map { |group| group.with_indifferent_access.slice(:id, :name) }
end
result
end
def provides_groups?
SiteSetting.google_oauth2_hd.present? &&
SiteSetting.google_oauth2_hd_groups &&
SiteSetting.google_oauth2_hd_groups_service_account_admin_email.present? &&
SiteSetting.google_oauth2_hd_groups_service_account_json.present?
end
private
def raw_groups(uid)
groups = []
page_token = nil
groups_url = "https://#{GROUPS_DOMAIN}#{GROUPS_PATH}"
client = build_service_account_client
return if client.nil?
loop do
params = {
userKey: uid
}
params[:pageToken] = page_token if page_token
response = client.get(groups_url, params: params, raise_errors: false)
if response.status == 200
response = response.parsed
groups.push(*response['groups'])
page_token = response['nextPageToken']
break if page_token.nil?
else
Rails.logger.error("[Discourse Google OAuth2] failed to retrieve groups for #{uid} - status #{response.status}")
break
end
end
groups
end
def build_service_account_client
service_account_info = JSON.parse(SiteSetting.google_oauth2_hd_groups_service_account_json)
payload = {
iss: service_account_info["client_email"],
aud: "#{OAUTH2_BASE_URL}/token",
scope: GROUPS_SCOPE,
iat: Time.now.to_i,
exp: Time.now.to_i + 60,
sub: SiteSetting.google_oauth2_hd_groups_service_account_admin_email
}
headers = { "alg" => "RS256", "typ" => "JWT" }
key = OpenSSL::PKey::RSA.new(service_account_info["private_key"])
encoded_jwt = ::JWT.encode(payload, key, 'RS256', headers)
client = OAuth2::Client.new(
SiteSetting.google_oauth2_client_id,
SiteSetting.google_oauth2_client_secret,
site: OAUTH2_BASE_URL
)
token_response = client.request(:post, '/token', body: {
grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
assertion: encoded_jwt
}, raise_errors: false)
if token_response.status != 200
Rails.logger.error("[Discourse Google OAuth2] failed to retrieve group fetch token - status #{token_response.status}")
return
end
OAuth2::AccessToken.from_hash(client, token_response.parsed)
end
end