diff --git a/app/controllers/openid_connect/authorizations_controller.rb b/app/controllers/openid_connect/authorizations_controller.rb index c10c137be..54f118715 100644 --- a/app/controllers/openid_connect/authorizations_controller.rb +++ b/app/controllers/openid_connect/authorizations_controller.rb @@ -1,47 +1,54 @@ -class AuthorizationsController < ApplicationController +class OpenidConnect::AuthorizationsController < ApplicationController rescue_from Rack::OAuth2::Server::Authorize::BadRequest do |e| @error = e - logger.info e.backtrace[0,10].join("\n") - render :error, status: e.status + print e.backtrace[0,10].join("\n") + render json: {error: :error, status: e.status} #error_message: e.message end before_action :authenticate_user! def new - call_authorization_endpoint + request_authorization_consent_form end def create - call_authorization_endpoint :is_create, params[:approve] + process_authorization_consent(params[:approve]) end - private +private - def call_authorization_endpoint(is_create = false, approved = false) - endpoint = AuthorizationEndpoint.new is_create, approved - rack_response = *endpoint.call(request.env) - @client, @response_type, @redirect_uri, @scopes, @_request_, @request_uri, @request_object = *[ - endpoint.client, endpoint.response_type, endpoint.redirect_uri, endpoint.scopes, endpoint._request_, endpoint.request_uri, endpoint.request_object + def request_authorization_consent_form + endpoint = OpenidConnect::Authorization::EndpointStartPoint.new(current_user) + endpoint.call(request.env) + @client, @response_type, @redirect_uri, @scopes, @request_object = *[ + endpoint.client, endpoint.response_type, endpoint.redirect_uri, endpoint.scopes, endpoint.request_object ] - if ( - !is_create && - (max_age = @request_object.try(:id_token).try(:max_age)) && - current_account.last_logged_in_at < max_age.seconds.ago - ) - flash[:notice] = 'Exceeded Max Age, Login Again' - unauthenticate! - end - respond_as_rack_app *rack_response + saveRequestParameters + render :new end - def respond_as_rack_app(status, header, response) - ["WWW-Authenticate"].each do |key| - headers[key] = header[key] if header[key].present? - end - if response.redirect? - redirect_to header['Location'] - else - render :new - end + def process_authorization_consent(approvedString) + endpoint = OpenidConnect::Authorization::EndpointConfirmationPoint.new(current_user, to_boolean(approvedString)) + restoreRequestParameters(endpoint) + status, header, response = *endpoint.call(request.env) + redirect_to header['Location'] + end + + def saveRequestParameters + session[:client_id], session[:response_type], session[:redirect_uri], session[:scopes], session[:request_object] = + @client.client_id, @response_type, @redirect_uri, @scopes.collect { |scope| scope.name }, @request_object + end + + def restoreRequestParameters(endpoint) + req = Rack::Request.new(request.env) + req.update_param("client_id", session[:client_id]) + req.update_param("redirect_uri", session[:redirect_uri]) + req.update_param("response_type", session[:response_type]) + endpoint.scopes, endpoint.request_object = + session[:scopes].collect {|scope| Scope.find_by_name(scope)}, session[:request_object] + end + + def to_boolean(str) + str.downcase == "true" end end diff --git a/app/models/o_auth_application.rb b/app/models/o_auth_application.rb index 5ac6b9e63..1f9a12ccb 100644 --- a/app/models/o_auth_application.rb +++ b/app/models/o_auth_application.rb @@ -6,6 +6,8 @@ class OAuthApplication < ActiveRecord::Base validates :client_id, presence: true, uniqueness: true validates :client_secret, presence: true + serialize :redirect_uris, JSON + before_validation :setup, on: :create def setup self.client_id = SecureRandom.hex(16) @@ -13,6 +15,10 @@ class OAuthApplication < ActiveRecord::Base end class << self + def available_response_types + ["id_token"] + end + def register!(registrarHash) registrarHash.validate! buildClientApplication(registrarHash) diff --git a/app/views/openid_connect/authorizations/_form.html.erb b/app/views/openid_connect/authorizations/_form.html.erb new file mode 100644 index 000000000..cce51e116 --- /dev/null +++ b/app/views/openid_connect/authorizations/_form.html.erb @@ -0,0 +1,9 @@ +<%= form_tag openid_connect_authorizations_path, class: action do %> + <% if action == :approve %> + <%= submit_tag "Approve" %> + <%= hidden_field_tag :approve, true %> + <% else %> + <%= submit_tag "Deny" %> + <%= hidden_field_tag :approve, false %> + <% end %> +<% end %> diff --git a/app/views/openid_connect/authorizations/new.html.erb b/app/views/openid_connect/authorizations/new.html.erb new file mode 100644 index 000000000..ee26ee058 --- /dev/null +++ b/app/views/openid_connect/authorizations/new.html.erb @@ -0,0 +1,15 @@ +
You will be redirected to <%= @redirect_uri %> with an id token if approved or an error if denied
+<%= JSON.pretty_generate @request_object.as_json %>+ + <% end %> + +<%= render 'openid_connect/authorizations/form', action: :approve %> +<%= render 'openid_connect/authorizations/form', action: :deny %> diff --git a/lib/openid_connect/authorization/endpoint.rb b/lib/openid_connect/authorization/endpoint.rb index cf2b359f3..885e17022 100644 --- a/lib/openid_connect/authorization/endpoint.rb +++ b/lib/openid_connect/authorization/endpoint.rb @@ -7,11 +7,7 @@ module OpenidConnect def initialize(current_user) @user = current_user @app = Rack::OAuth2::Server::Authorize.new do |req, res| - buildClient(req) - buildRedirectURI(req, res) - verifyNonce(req, res) - buildScopes(req) - buildRequestObject(req) + buildAttributes(req, res) if OAuthApplication.available_response_types.include? Array(req.response_type).collect(&:to_s).join(' ') handleResponseType(req, res) else @@ -19,32 +15,23 @@ module OpenidConnect end end end + def buildAttributes(req, res) + buildClient(req) + buildRedirectURI(req, res) + end + + def handleResponseType(req, res) + # Implemented by subclass + end + + private + def buildClient(req) @client = OAuthApplication.find_by_client_id(req.client_id) || req.bad_request! end def buildRedirectURI(req, res) res.redirect_uri = @redirect_uri = req.verify_redirect_uri!(@client.redirect_uris) end - def verifyNonce(req, res) - if res.protocol_params_location == :fragment && req.nonce.blank? - req.invalid_request! 'nonce required' - end - end - def buildScopes(req) - @scopes = req.scope.inject([]) do |_scopes_, scope| - _scopes_ << Scope.find_by_name(scope) or req.invalid_scope! "Unknown scope: #{scope}" - end - end - def buildRequestObject(req) - @request_object = if (@_request_ = req.request).present? - OpenIDConnect::RequestObject.decode req.request, nil # @client.secret - elsif (@request_uri = req.request_uri).present? - OpenIDConnect::RequestObject.fetch req.request_uri, nil # @client.secret - end - end - def handleResponseType(req, res) - # Implemented by subclass - end end end end diff --git a/lib/openid_connect/authorization/endpoint_confirmation_point.rb b/lib/openid_connect/authorization/endpoint_confirmation_point.rb new file mode 100644 index 000000000..ff171e53e --- /dev/null +++ b/lib/openid_connect/authorization/endpoint_confirmation_point.rb @@ -0,0 +1,35 @@ +module OpenidConnect + module Authorization + class EndpointConfirmationPoint < Endpoint + def initialize(current_user, approved = false) + super(current_user) + @approved = approved + end + + def buildAttributes(req, res) + super(req, res) + # TODO: buildResponseType(req) + end + + def handleResponseType(req, res) + handleApproval(@approved, req, res) + end + + def handleApproval(approved, req, res) + if approved + approved!(req, res) + else + req.access_denied! + end + end + + def approved!(req, res) + response_types = Array(req.response_type) + if response_types.include?(:id_token) + res.id_token = SecureRandom.hex(16) # TODO: Replace with real ID token + end + res.approve! + end + end + end +end diff --git a/lib/openid_connect/authorization/endpoint_start_point.rb b/lib/openid_connect/authorization/endpoint_start_point.rb new file mode 100644 index 000000000..8fa6e4fa6 --- /dev/null +++ b/lib/openid_connect/authorization/endpoint_start_point.rb @@ -0,0 +1,28 @@ +module OpenidConnect + module Authorization + class EndpointStartPoint < Endpoint + def initialize(current_user) + super(current_user) + end + def handleResponseType(req, res) + @response_type = req.response_type + end + def buildAttributes(req, res) + super(req, res) + verifyNonce(req, res) + buildScopes(req) + # TODO: buildRequestObject(req) + end + def verifyNonce(req, res) + if res.protocol_params_location == :fragment && req.nonce.blank? + req.invalid_request! "nonce required" + end + end + def buildScopes(req) + @scopes = req.scope.inject([]) do |_scopes_, scope| + _scopes_ << Scope.find_by_name(scope) or req.invalid_scope! "Unknown scope: #{scope}" + end + end + end + end +end diff --git a/spec/controllers/openid_connect/authorizations_controller_spec.rb b/spec/controllers/openid_connect/authorizations_controller_spec.rb new file mode 100644 index 000000000..7852408ca --- /dev/null +++ b/spec/controllers/openid_connect/authorizations_controller_spec.rb @@ -0,0 +1,69 @@ +require 'spec_helper' + +describe OpenidConnect::AuthorizationsController, type: :controller do + let!(:client) { OAuthApplication.create!(redirect_uris: ["http://localhost:3000/"]) } + + before do + sign_in :user, alice + allow(@controller).to receive(:current_user).and_return(alice) + Scope.create!(name:"openid") + end + + describe "#new" do + render_views + context "when valid parameters are passed" do + it "should return a form page" do + get :new, + { + client_id: client.client_id, + redirect_uri: "http://localhost:3000/", + response_type: "id_token", + scope: "openid", + nonce: SecureRandom.hex(16), + state: SecureRandom.hex(16) + } + expect(response.body).to match("Approve") + expect(response.body).to match("Deny") + end + end + # TODO: Implement tests for missing/invalid parameters + end + + describe "#create" do + before do + get :new, + { + client_id: client.client_id, + redirect_uri: "http://localhost:3000/", + response_type: "id_token", + scope: "openid", + nonce: SecureRandom.hex(16), + state: SecureRandom.hex(16) + } + end + context "when authorization is approved" do + it "should return the id token in a fragment" do + post :create, + { + approve: "true" + } + expect(response.location).to have_content("#id_token=") + end + end + context "when authorization is denied" do + before do + post :create, + { + approve: "false" + } + end + it "should return an error in the fragment" do + expect(response.location).to have_content("#error=") + end + it "should NOT contain a id token in the fragment" do + expect(response.location).to_not have_content("#id_token=") + end + end + end + +end