class Rs::OauthController < ApplicationController skip_before_action :verify_authenticity_token, only: :token prepend_before_action :assert_redirect_uri, only: [:new, :create] before_action :require_signed_in_with_username, only: :new before_action :authenticate_user!, only: :create before_action :set_cors_headers, only: :token def new @user = User.where(cn: params[:username].downcase, ou: Setting.primary_domain).first @scopes = parse_scopes params[:scope] @redirect_uri = params[:redirect_uri] @client_id = params[:client_id] @state = params[:state] @response_type = params[:response_type].presence || "token" @code_challenge = params[:code_challenge] @code_challenge_method = params[:code_challenge_method].presence || "S256" @root_access_requested = (@scopes & [":r",":rw"]).any? @denial_url = error_url(@redirect_uri, "error=access_denied", @state) @expire_at_dates = [["Never", nil], ["In 1 month", 1.month.from_now], ["In 1 day", 1.day.from_now]] unless current_user == @user sign_out :user redirect_params = { scope: params[:scope], redirect_uri: params[:redirect_uri], client_id: params[:client_id], state: params[:state] } if @response_type == "code" redirect_params[:response_type] = "code" redirect_params[:code_challenge] = @code_challenge redirect_params[:code_challenge_method] = @code_challenge_method end redirect_to new_rs_oauth_url(@user.cn, redirect_params) return end unless @client_id.present? error_redirect(@redirect_uri, "error=invalid_request", @state) and return end if @scopes.empty? error_redirect(@redirect_uri, "error=invalid_scope", @state) and return end unless hostname_of(@client_id) == hostname_of(@redirect_uri) error_redirect(@redirect_uri, "error=invalid_client", @state) and return end unless %w[token code].include?(@response_type) error_redirect(@redirect_uri, "error=unsupported_response_type", @state) and return end if @response_type == "code" && !valid_code_challenge?(@code_challenge, @code_challenge_method) error_redirect(@redirect_uri, "error=invalid_request", @state) and return end @client_id.gsub!(/http(s)?:\/\//, "") end def create unless current_user.id.to_s == params[:user_id] Rails.logger.info("NO MATCH: #{params[:user_id]}, #{current_user.id}") http_status :forbidden and return end response_type = params[:response_type].presence || "token" permissions = parse_scopes params[:scope] redirect_uri = params[:redirect_uri].presence client_id = params[:client_id].presence state = params[:state].presence expire_at = params[:expire_at].presence if permissions.empty? error_redirect(redirect_uri, "error=invalid_scope", state, response_type) and return end unless client_id.present? error_redirect(redirect_uri, "error=invalid_request", state, response_type) and return end unless hostname_of(client_id) == hostname_of(redirect_uri) error_redirect(redirect_uri, "error=invalid_client", state, response_type) and return end unless %w[token code].include?(response_type) error_redirect(redirect_uri, "error=unsupported_response_type", state, response_type) and return end client_id.gsub!(/http(s)?:\/\//, "") if response_type == "code" challenge = params[:code_challenge].presence method = params[:code_challenge_method].presence || "S256" unless valid_code_challenge?(challenge, method) error_redirect(redirect_uri, "error=invalid_request", state, response_type) and return end code = RsOauthManager::IssueCode.call( user: current_user, client_id: client_id, redirect_uri: redirect_uri, permissions: permissions, expire_at: expire_at, user_agent: request.user_agent, code_challenge: challenge, code_challenge_method: method ) redirect_to url_with_state("#{redirect_uri}#{query_separator(redirect_uri)}code=#{CGI.escape(code.code)}", state), allow_other_host: true else auth = current_user.remote_storage_authorizations.create!( permissions: permissions, client_id: client_id, redirect_uri: redirect_uri, app_name: client_id, expire_at: expire_at, user_agent: request.user_agent ) redirect_to url_with_state("#{redirect_uri}#access_token=#{auth.token}", state), allow_other_host: true end end def token return head :ok if request.options? unless params[:grant_type] == "authorization_code" return render_token_error("unsupported_grant_type") end if params[:code].blank? || params[:code_verifier].blank? || params[:client_id].blank? || params[:redirect_uri].blank? return render_token_error("invalid_request") end auth = RsOauthManager::ExchangeCode.call( code: params[:code], client_id: params[:client_id], redirect_uri: params[:redirect_uri], code_verifier: params[:code_verifier] ) render_token(token_response(auth)) rescue RsOauthManagerService::InvalidGrant render_token_error("invalid_grant") end private def assert_redirect_uri http_status :bad_request unless params[:redirect_uri].present? end def require_signed_in_with_username unless user_signed_in? session[:user_return_to] = request.url redirect_to new_user_session_path(cn: params[:username], ou: Setting.primary_domain) end end def set_cors_headers headers["Access-Control-Allow-Origin"] = "*" headers["Access-Control-Allow-Methods"] = "POST, OPTIONS" headers["Access-Control-Allow-Headers"] = "Content-Type" end def token_response(auth) body = { access_token: auth.token, token_type: "bearer", scope: auth.permissions.join(" ") } if auth.expire_at.present? body[:expires_in] = (auth.expire_at - Time.current).to_i end body end def hostname_of(uri) RsOauthManagerService.hostname_of(uri) end def parse_scopes(scope_string) RsOauthManagerService.parse_scopes(scope_string) end def error_redirect(redirect_uri, error, state, response_type = @response_type) redirect_to error_url(redirect_uri, error, state, response_type), allow_other_host: true end def error_url(redirect_uri, error, state, response_type = @response_type) separator = response_type == "code" ? query_separator(redirect_uri) : "#" url_with_state("#{redirect_uri}#{separator}#{error}", state) end def query_separator(uri) uri.to_s.include?("?") ? "&" : "?" end def valid_code_challenge?(challenge, method) method == "S256" && challenge.to_s.match?(/\A[A-Za-z0-9\-._~]{43,128}\z/) end def render_token(body) response.headers["Cache-Control"] = "no-store" render json: body end def render_token_error(error) response.headers["Cache-Control"] = "no-store" render json: { error: error }, status: :bad_request end def url_with_state(url, state) state ? "#{url}&state=#{CGI.escape(state)}" : url end end