226 lines
7.9 KiB
Ruby
226 lines
7.9 KiB
Ruby
|
require_dependency 'carto/errors'
|
||
|
require_dependency 'carto/oauth_provider/errors'
|
||
|
require_dependency 'carto/oauth_provider/grant_strategies'
|
||
|
require_dependency 'carto/oauth_provider/response_strategies'
|
||
|
require_dependency 'carto/oauth_provider/token_presenter'
|
||
|
|
||
|
module Carto
|
||
|
class OauthProviderController < ApplicationController
|
||
|
GRANT_STRATEGIES = {
|
||
|
'authorization_code' => OauthProvider::GrantStrategies::AuthorizationCodeStrategy,
|
||
|
'refresh_token' => OauthProvider::GrantStrategies::RefreshTokenStrategy
|
||
|
}.freeze
|
||
|
|
||
|
RESPONSE_STRATEGIES = {
|
||
|
'code' => OauthProvider::ResponseStrategies::CodeStrategy,
|
||
|
'token' => OauthProvider::ResponseStrategies::TokenStrategy
|
||
|
}.freeze
|
||
|
|
||
|
REQUIRED_TOKEN_PARAMS = ['client_id', 'client_secret', 'grant_type'].freeze
|
||
|
REQUIRED_AUTHORIZE_PARAMS = ['client_id', 'state', 'response_type'].freeze
|
||
|
|
||
|
SILENT_PROMPT_VALUE = 'none'.freeze
|
||
|
|
||
|
ssl_required
|
||
|
|
||
|
layout 'frontend'
|
||
|
|
||
|
skip_before_action :ensure_org_url_if_org_user
|
||
|
skip_before_action :verify_authenticity_token, only: [:token]
|
||
|
|
||
|
before_action :allow_silent_flow_iframe, only: :consent, if: :silent_flow?
|
||
|
before_action :set_redirection_error_handling, only: [:consent, :authorize]
|
||
|
before_action :ensure_required_token_params, only: [:token]
|
||
|
before_action :load_oauth_app, :verify_redirect_uri
|
||
|
before_action :login_required_any_user, only: [:consent, :authorize]
|
||
|
before_action :validate_prompt_request, only: [:consent]
|
||
|
before_action :reject_client_secret, only: [:consent, :authorize]
|
||
|
before_action :ensure_required_authorize_params, only: [:consent, :authorize]
|
||
|
before_action :validate_response_type, :validate_scopes, :set_state, only: [:consent, :authorize]
|
||
|
before_action :load_oauth_app_user, only: [:consent, :authorize]
|
||
|
before_action :validate_grant_type, :verify_client_secret, only: [:token]
|
||
|
|
||
|
rescue_from StandardError, with: :rescue_generic_errors
|
||
|
rescue_from Carto::MissingParamsError, with: :rescue_missing_params_error
|
||
|
rescue_from OauthProvider::Errors::BaseError, with: :rescue_oauth_errors
|
||
|
|
||
|
def consent
|
||
|
return create_authorization_code if @oauth_app_user.try(:authorized?, @scopes)
|
||
|
raise OauthProvider::Errors::AccessDenied.new if silent_flow?
|
||
|
|
||
|
unless @oauth_app_user
|
||
|
oauth_app_user = @oauth_app.oauth_app_users.new(user_id: current_viewer.id, scopes: @scopes)
|
||
|
validate_oauth_app_user(oauth_app_user)
|
||
|
end
|
||
|
|
||
|
@scopes_by_category = OauthProvider::Scopes.scopes_by_category(@scopes, @oauth_app_user.try(:all_scopes))
|
||
|
end
|
||
|
|
||
|
def authorize
|
||
|
raise OauthProvider::Errors::AccessDenied.new unless params[:accept]
|
||
|
|
||
|
if @oauth_app_user
|
||
|
@oauth_app_user.upgrade!(@scopes)
|
||
|
else
|
||
|
@oauth_app_user = @oauth_app.oauth_app_users.new(user_id: current_viewer.id, scopes: @scopes)
|
||
|
validate_oauth_app_user(@oauth_app_user)
|
||
|
@oauth_app_user.save!
|
||
|
track_event
|
||
|
end
|
||
|
|
||
|
create_authorization_code
|
||
|
end
|
||
|
|
||
|
def token
|
||
|
access_token, refresh_token = grant_strategy.authorize!(@oauth_app, params)
|
||
|
|
||
|
render(json: OauthProvider::TokenPresenter.new(access_token, refresh_token: refresh_token).to_hash)
|
||
|
end
|
||
|
|
||
|
def not_authorized
|
||
|
raise OauthProvider::Errors::LoginRequired.new if silent_flow?
|
||
|
super
|
||
|
end
|
||
|
|
||
|
private
|
||
|
|
||
|
def create_authorization_code
|
||
|
response = response_strategy.authorize!(
|
||
|
@oauth_app_user, redirect_uri: @redirect_uri, scopes: @scopes, state: @state
|
||
|
)
|
||
|
redirect_to_oauth_app(response)
|
||
|
end
|
||
|
|
||
|
def redirect_to_oauth_app(parameters)
|
||
|
redirect_to response_strategy.build_redirect_uri(@redirect_uri || @oauth_app.redirect_uris.first, parameters)
|
||
|
end
|
||
|
|
||
|
def set_redirection_error_handling
|
||
|
@redirect_on_error = true
|
||
|
end
|
||
|
|
||
|
def rescue_oauth_errors(exception)
|
||
|
CartoDB::Logger.debug(message: 'Oauth provider error',
|
||
|
exception: exception,
|
||
|
redirect_on_error: @redirect_on_error,
|
||
|
oauth_app: @oauth_app)
|
||
|
|
||
|
if @redirect_on_error && @oauth_app && response_strategy
|
||
|
redirect_to_oauth_app(exception.parameters.merge(state: @state))
|
||
|
elsif @redirect_on_error
|
||
|
@error = exception.error_message
|
||
|
render 'consent.html', status: 400
|
||
|
else
|
||
|
render json: exception.parameters, status: 400
|
||
|
end
|
||
|
end
|
||
|
|
||
|
def rescue_generic_errors(exception)
|
||
|
CartoDB::Logger.error(exception: exception)
|
||
|
if exception.is_a?(Carto::RelationDoesNotExistError)
|
||
|
return rescue_oauth_errors(OauthProvider::Errors::InvalidScope.new(nil, message: exception.user_message))
|
||
|
end
|
||
|
rescue_oauth_errors(OauthProvider::Errors::ServerError.new)
|
||
|
end
|
||
|
|
||
|
def rescue_missing_params_error(exception)
|
||
|
rescue_oauth_errors(OauthProvider::Errors::InvalidRequest.new(exception.message))
|
||
|
end
|
||
|
|
||
|
def validate_response_type
|
||
|
@response_type = params[:response_type]
|
||
|
|
||
|
raise OauthProvider::Errors::UnsupportedResponseType.new(RESPONSE_STRATEGIES.keys) unless response_strategy
|
||
|
end
|
||
|
|
||
|
def validate_grant_type
|
||
|
raise OauthProvider::Errors::UnsupportedGrantType.new(GRANT_STRATEGIES.keys) unless grant_strategy
|
||
|
end
|
||
|
|
||
|
def load_oauth_app
|
||
|
@oauth_app = OauthApp.find_by_client_id!(params[:client_id])
|
||
|
rescue ActiveRecord::RecordNotFound
|
||
|
raise OauthProvider::Errors::InvalidClient.new
|
||
|
end
|
||
|
|
||
|
def verify_redirect_uri
|
||
|
# Redirect URI is optional but, if present, must match a registered URI
|
||
|
@redirect_uri = params[:redirect_uri].presence
|
||
|
if @redirect_uri.present? && !@oauth_app.redirect_uris.include?(@redirect_uri)
|
||
|
@redirect_uri = nil
|
||
|
raise OauthProvider::Errors::InvalidRequest.new('The redirect_uri must match the redirect_uri param used in the authorization request')
|
||
|
end
|
||
|
end
|
||
|
|
||
|
def validate_scopes
|
||
|
@scopes = (params[:scope] || '').split(' ')
|
||
|
|
||
|
invalid_scopes = OauthProvider::Scopes.invalid_scopes_and_tables(@scopes, current_viewer)
|
||
|
raise OauthProvider::Errors::InvalidScope.new(invalid_scopes) if invalid_scopes.present?
|
||
|
end
|
||
|
|
||
|
def set_state
|
||
|
@state = params[:state]
|
||
|
end
|
||
|
|
||
|
def load_oauth_app_user
|
||
|
@oauth_app_user = @oauth_app.oauth_app_users.find_by_user_id(current_viewer.try(:id))
|
||
|
end
|
||
|
|
||
|
def verify_client_secret
|
||
|
raise OauthProvider::Errors::InvalidClient.new unless params[:client_secret] == @oauth_app.client_secret
|
||
|
end
|
||
|
|
||
|
def ensure_required_token_params
|
||
|
grant_params = grant_strategy.try(:required_params) || []
|
||
|
ensure_required_params(REQUIRED_TOKEN_PARAMS + grant_params)
|
||
|
end
|
||
|
|
||
|
def ensure_required_authorize_params
|
||
|
ensure_required_params(REQUIRED_AUTHORIZE_PARAMS)
|
||
|
end
|
||
|
|
||
|
def reject_client_secret
|
||
|
raise OauthProvider::Errors::InvalidRequest.new("The client_secret param must not be sent in the authorize request") if params[:client_secret].present?
|
||
|
end
|
||
|
|
||
|
def validate_oauth_app_user(oauth_app_user)
|
||
|
unless oauth_app_user.valid?
|
||
|
errors = oauth_app_user.errors.full_messages_for(:user)
|
||
|
raise OauthProvider::Errors::AccessDenied.new(errors.join(', ')) if errors.present?
|
||
|
end
|
||
|
end
|
||
|
|
||
|
def validate_prompt_request
|
||
|
if params[:prompt].present?
|
||
|
raise OauthProvider::Errors::InvalidRequest.new("Only 'prompt=none' is supported") unless silent_flow?
|
||
|
end
|
||
|
end
|
||
|
|
||
|
def silent_flow?
|
||
|
params[:prompt] == SILENT_PROMPT_VALUE
|
||
|
end
|
||
|
|
||
|
def grant_strategy
|
||
|
GRANT_STRATEGIES[params[:grant_type]]
|
||
|
end
|
||
|
|
||
|
def response_strategy
|
||
|
RESPONSE_STRATEGIES[params[:response_type]]
|
||
|
end
|
||
|
|
||
|
def allow_silent_flow_iframe
|
||
|
response.headers.except! 'X-Frame-Options'
|
||
|
end
|
||
|
|
||
|
def track_event
|
||
|
properties = {
|
||
|
user_id: @oauth_app_user.user_id,
|
||
|
app_id: @oauth_app_user.oauth_app.id,
|
||
|
app_name: @oauth_app_user.oauth_app.name
|
||
|
}
|
||
|
Carto::Tracking::Events::CreatedOauthAppUser.new(current_viewer.id, properties).report
|
||
|
end
|
||
|
end
|
||
|
end
|