From 25f51c606a7b5762919e2b37fb12c786477da1da Mon Sep 17 00:00:00 2001 From: theworldbright Date: Thu, 6 Aug 2015 19:41:40 +0900 Subject: [PATCH] Add support for prompt parameter --- .../authorizations_controller.rb | 74 +++++++++++++++++-- app/presenters/user_applications_presenter.rb | 2 +- .../step_definitions/implicit_flow_steps.rb | 1 + .../authorizations_controller_spec.rb | 68 +++++++++++++++++ 4 files changed, 139 insertions(+), 6 deletions(-) diff --git a/app/controllers/api/openid_connect/authorizations_controller.rb b/app/controllers/api/openid_connect/authorizations_controller.rb index 25bc0d957..148441fb9 100644 --- a/app/controllers/api/openid_connect/authorizations_controller.rb +++ b/app/controllers/api/openid_connect/authorizations_controller.rb @@ -9,7 +9,13 @@ module Api before_action :authenticate_user! def new - request_authorization_consent_form + auth = Api::OpenidConnect::Authorization.find_by_client_id_and_user(params[:client_id], current_user) + if params[:prompt] + prompt = params[:prompt].split(" ") + handle_prompt(prompt, auth) + else + handle_authorization_form(auth) + end end def create @@ -29,12 +35,50 @@ module Api private - def request_authorization_consent_form # TODO: Add support for prompt params - if Api::OpenidConnect::Authorization.find_by_client_id_and_user(params[:client_id], current_user) + def handle_prompt(prompt, auth) + if prompt.include? "select_account" + handle_prompt_params_error("account_selection_required", + "There is no support for choosing among multiple accounts") + elsif prompt.include? "none" + handle_prompt_none(prompt, auth) + elsif prompt.include?("login") && logged_in_more_than_5_minutes_ago? + handle_prompt_params_error("login_required", + "There is no support for re-authenticating already authenticated users") + elsif prompt.include? "consent" + request_authorization_consent_form + else + handle_authorization_form(auth) + end + end + + def handle_authorization_form(auth) + if auth process_authorization_consent("true") else - endpoint = Api::OpenidConnect::AuthorizationPoint::EndpointStartPoint.new(current_user) - handle_start_point_response(endpoint) + request_authorization_consent_form + end + end + + def request_authorization_consent_form + endpoint = Api::OpenidConnect::AuthorizationPoint::EndpointStartPoint.new(current_user) + handle_start_point_response(endpoint) + end + + def logged_in_more_than_5_minutes_ago? + (current_user.current_sign_in_at.to_i - Time.zone.now.to_i) > 300 + end + + def handle_prompt_none(prompt, auth) + if prompt == ["none"] + if auth + process_authorization_consent("true") + else + handle_prompt_params_error("interaction_required", + "The Authentication Request cannot be completed without end-user interaction") + end + else + handle_prompt_params_error("invalid_request", + "The 'none' value cannot be used with any other prompt value") end end @@ -115,6 +159,26 @@ module Api session[:response_type] end end + + def handle_prompt_params_error(error, error_description) + if params[:client_id] && params[:redirect_uri] + app = Api::OpenidConnect::OAuthApplication.find_by(client_id: params[:client_id]) + if app && app.redirect_uris.include?(params[:redirect_uri]) + redirect_prompt_error_display(error, error_description) + else + render json: {error: "bad_request", + description: "No client with client_id " + params[:client_id] + " found"} + end + else + render json: {error: "bad_request", description: "Missing client id or redirect URI"} + end + end + + def redirect_prompt_error_display(error, error_description) + redirect_params_hash = {error: error, error_description: error_description, state: params[:state]} + redirect_fragment = redirect_params_hash.compact.map {|key, value| key.to_s + "=" + value }.join("&") + redirect_to params[:redirect_uri] + "#" + redirect_fragment + end end end end diff --git a/app/presenters/user_applications_presenter.rb b/app/presenters/user_applications_presenter.rb index 5a0ad9cfc..8c2d263fd 100644 --- a/app/presenters/user_applications_presenter.rb +++ b/app/presenters/user_applications_presenter.rb @@ -31,7 +31,7 @@ class UserApplicationsPresenter def find_scopes(application) scopes = Api::OpenidConnect::Authorization.find_by_client_id_and_user( application.client_id, @current_user).scopes - scopes.each_with_object([]){|scope, array| array << scope.name } + scopes.each_with_object([]) {|scope, array| array << scope.name } end def find_id(application) diff --git a/features/step_definitions/implicit_flow_steps.rb b/features/step_definitions/implicit_flow_steps.rb index 4516ab55d..844cf3f96 100644 --- a/features/step_definitions/implicit_flow_steps.rb +++ b/features/step_definitions/implicit_flow_steps.rb @@ -4,6 +4,7 @@ o_auth_query_params = %i( scope=openid%20read nonce=hello state=hi + prompt=login ).join("&") Given /^I send a post request from that client to the implicit flow authorization endpoint$/ do diff --git a/spec/controllers/api/openid_connect/authorizations_controller_spec.rb b/spec/controllers/api/openid_connect/authorizations_controller_spec.rb index fdc69afad..4e616388a 100644 --- a/spec/controllers/api/openid_connect/authorizations_controller_spec.rb +++ b/spec/controllers/api/openid_connect/authorizations_controller_spec.rb @@ -84,6 +84,51 @@ describe Api::OpenidConnect::AuthorizationsController, type: :controller do expect(response.location).to match("error=invalid_request") end end + + context "when prompt is none" do + it "should return an interaction required error" do + post :new, client_id: client.client_id, redirect_uri: "http://localhost:3000/", + response_type: "id_token", scope: "openid", state: 1234, display: "page", prompt: "none" + expect(response.location).to match("error=interaction_required") + expect(response.location).to match("state=1234") + end + end + + context "when prompt is none and consent" do + it "should return an interaction required error" do + post :new, client_id: client.client_id, redirect_uri: "http://localhost:3000/", + response_type: "id_token", scope: "openid", state: 1234, display: "page", prompt: "none consent" + expect(response.location).to match("error=invalid_request") + expect(response.location).to match("state=1234") + end + end + + context "when prompt is select_account" do + it "should return an account_selection_required error" do + post :new, client_id: client.client_id, redirect_uri: "http://localhost:3000/", + response_type: "id_token", scope: "openid", state: 1234, display: "page", prompt: "select_account" + expect(response.location).to match("error=account_selection_required") + expect(response.location).to match("state=1234") + end + end + + context "when prompt is none and client ID is invalid" do + it "should return an account_selection_required error" do + post :new, client_id: "random", redirect_uri: "http://localhost:3000/", + response_type: "id_token", scope: "openid", state: 1234, display: "page", prompt: "none" + json_body = JSON.parse(response.body) + expect(json_body["error"]).to match("bad_request") + end + end + + context "when prompt is none and redirect URI does not match pre-registered URIs" do + it "should return an account_selection_required error" do + post :new, client_id: client.client_id, redirect_uri: "http://randomuri:3000/", + response_type: "id_token", scope: "openid", state: 1234, display: "page", prompt: "none" + json_body = JSON.parse(response.body) + expect(json_body["error"]).to match("bad_request") + end + end end context "when already authorized" do let!(:auth) { @@ -110,6 +155,29 @@ describe Api::OpenidConnect::AuthorizationsController, type: :controller do expect(response.location).to have_content("state=4130930983") end end + + context "when prompt is none" do + it "should return the id token in a fragment" do + post :new, client_id: client.client_id, redirect_uri: "http://localhost:3000/", + response_type: "id_token", scope: "openid", nonce: 413_093_098_3, state: 413_093_098_3, + display: "page", prompt: "none" + expect(response.location).to have_content("id_token=") + encoded_id_token = response.location[/(?<=id_token=)[^&]+/] + decoded_token = OpenIDConnect::ResponseObject::IdToken.decode encoded_id_token, + Api::OpenidConnect::IdTokenConfig.public_key + expect(decoded_token.nonce).to eq("4130930983") + expect(decoded_token.exp).to be > Time.zone.now.utc.to_i + end + end + + context "when prompt contains consent" do + it "should return a consent form page" do + get :new, client_id: client.client_id, redirect_uri: "http://localhost:3000/", + response_type: "id_token", scope: "openid", nonce: 413_093_098_3, state: 413_093_098_3, + display: "page", prompt: "consent" + expect(response.body).to match("Diaspora Test Client") + end + end end end