diff --git a/.gitignore b/.gitignore index 12dea17..a6f0c0b 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ spec/dummy/db/development.sqlite3 spec/dummy/log/development.log spec/dummy/log/test.log spec/dummy/tmp +docs diff --git a/app/controllers/stormpath/rails/id_site_login/new_controller.rb b/app/controllers/stormpath/rails/id_site_login/new_controller.rb new file mode 100644 index 0000000..fe5188f --- /dev/null +++ b/app/controllers/stormpath/rails/id_site_login/new_controller.rb @@ -0,0 +1,57 @@ +module Stormpath + module Rails + module IdSiteLogin + class NewController < BaseController + before_action :require_no_authentication! + + def call + begin + result = Stormpath::Rails::Client.application.handle_id_site_callback(request.url) + account = Stormpath::Rails::Client.client.accounts.get(result.account_href) + login_the_account(account) + respond_with_success(account) + rescue Stormpath::Error, JWT::VerificationError => error + respond_with_error(error) + end + end + + private + + def login_the_account(account) + AccountLoginWithStormpathToken.new( + cookies, account, + Stormpath::Rails::Client.application, + Stormpath::Rails::Client.client.data_store.api_key + ).call + end + + def respond_with_success(account) + respond_to do |format| + format.html { redirect_to login_redirect_route, notice: 'Successfully signed in' } + format.json { render json: AccountSerializer.to_h(account) } + end + end + + def respond_with_error(error) + respond_to do |format| + format.html do + flash.now[:error] = error.message + render stormpath_config.web.login.view + end + format.json do + render json: { message: error.message }, status: error.try(:status) + end + end + end + + def login_redirect_route + if params[:next] + URI(params[:next]).path + else + stormpath_config.web.login.next_uri + end + end + end + end + end +end diff --git a/app/controllers/stormpath/rails/id_site_logout/new_controller.rb b/app/controllers/stormpath/rails/id_site_logout/new_controller.rb new file mode 100644 index 0000000..f7e6b0e --- /dev/null +++ b/app/controllers/stormpath/rails/id_site_logout/new_controller.rb @@ -0,0 +1,19 @@ +module Stormpath + module Rails + module IdSiteLogout + class NewController < BaseController + def call + TokenAndCookiesCleaner.new(cookies).remove + redirect_to callback_url + end + + private + + def callback_url + Stormpath::Rails::Client.application.create_id_site_url(callback_uri: root_url, + logout: true) + end + end + end + end +end diff --git a/app/controllers/stormpath/rails/login/new_controller.rb b/app/controllers/stormpath/rails/login/new_controller.rb index 30169b4..0a4f1d3 100644 --- a/app/controllers/stormpath/rails/login/new_controller.rb +++ b/app/controllers/stormpath/rails/login/new_controller.rb @@ -6,7 +6,7 @@ class NewController < BaseController def call if stormpath_config.web.id_site.enabled - redirect_to id_site_login_url + redirect_to callback_url else respond_to do |format| format.json { render json: LoginNewSerializer.to_h } @@ -14,6 +14,15 @@ def call end end end + + private + + def callback_url + Stormpath::Rails::Client.application.create_id_site_url( + callback_uri: id_site_result_url, + path: Stormpath::Rails.config.web.id_site.login_uri + ) + end end end end diff --git a/app/controllers/stormpath/rails/logout/create_controller.rb b/app/controllers/stormpath/rails/logout/create_controller.rb index e503b8e..0b1dc4c 100644 --- a/app/controllers/stormpath/rails/logout/create_controller.rb +++ b/app/controllers/stormpath/rails/logout/create_controller.rb @@ -8,8 +8,7 @@ def call if bearer_authorization_header? DeleteAccessToken.call(bearer_access_token) else - delete_tokens - delete_cookies + TokenAndCookiesCleaner.new(cookies).remove end respond_with_success end @@ -28,24 +27,6 @@ def authorization_header request.headers['Authorization'] end - def delete_tokens - DeleteAccessToken.call(cookies[access_token_cookie_name]) - DeleteRefreshToken.call(cookies[refresh_token_cookie_name]) - end - - def delete_cookies - cookies.delete(access_token_cookie_name) - cookies.delete(refresh_token_cookie_name) - end - - def access_token_cookie_name - stormpath_config.web.access_token_cookie.name - end - - def refresh_token_cookie_name - stormpath_config.web.refresh_token_cookie.name - end - def respond_with_success respond_to do |format| format.html do diff --git a/app/controllers/stormpath/rails/register/new_controller.rb b/app/controllers/stormpath/rails/register/new_controller.rb index 5a0ff63..d5fbb12 100644 --- a/app/controllers/stormpath/rails/register/new_controller.rb +++ b/app/controllers/stormpath/rails/register/new_controller.rb @@ -4,7 +4,7 @@ module Register class NewController < BaseController def call if stormpath_config.web.id_site.enabled - redirect_to id_site_register_url + redirect_to callback_url elsif signed_in? redirect_to root_path else @@ -14,6 +14,15 @@ def call end end end + + private + + def callback_url + Stormpath::Rails::Client.application.create_id_site_url( + callback_uri: id_site_result_url, + path: Stormpath::Rails.config.web.id_site.register_uri + ) + end end end end diff --git a/app/services/stormpath/rails/token_and_cookies_cleaner.rb b/app/services/stormpath/rails/token_and_cookies_cleaner.rb new file mode 100644 index 0000000..f2b68eb --- /dev/null +++ b/app/services/stormpath/rails/token_and_cookies_cleaner.rb @@ -0,0 +1,36 @@ +module Stormpath + module Rails + class TokenAndCookiesCleaner + attr_reader :cookies + + def initialize(cookies) + @cookies = cookies + end + + def remove + delete_tokens + delete_cookies + end + + private + + def delete_tokens + DeleteAccessToken.call(cookies[access_token_cookie_name]) + DeleteRefreshToken.call(cookies[refresh_token_cookie_name]) + end + + def delete_cookies + cookies.delete(access_token_cookie_name) + cookies.delete(refresh_token_cookie_name) + end + + def access_token_cookie_name + Stormpath::Rails.config.web.access_token_cookie.name + end + + def refresh_token_cookie_name + Stormpath::Rails.config.web.refresh_token_cookie.name + end + end + end +end diff --git a/lib/stormpath/rails/router.rb b/lib/stormpath/rails/router.rb index 9513c43..4243f95 100644 --- a/lib/stormpath/rails/router.rb +++ b/lib/stormpath/rails/router.rb @@ -15,7 +15,9 @@ module Router 'oauth2#new' => 'stormpath/rails/oauth2/new#call', 'oauth2#create' => 'stormpath/rails/oauth2/create#call', 'verify_email#show' => 'stormpath/rails/verify_email/show#call', - 'verify_email#create' => 'stormpath/rails/verify_email/create#call' + 'verify_email#create' => 'stormpath/rails/verify_email/create#call', + 'id_site_login#new' => 'stormpath/rails/id_site_login/new#call', + 'id_site_logout#new' => 'stormpath/rails/id_site_logout/new#call' }.freeze def stormpath_rails_routes(actions: {}) @@ -66,6 +68,12 @@ def stormpath_rails_routes(actions: {}) get Stormpath::Rails.config.web.verify_email.uri => actions['verify_email#show'], as: :new_verify_email post Stormpath::Rails.config.web.verify_email.uri => actions['verify_email#create'], as: :verify_email end + + # ID SITE LOGIN + if Stormpath::Rails.config.web.id_site.enabled + get '/id_site_result' => actions['id_site_login#new'], as: :id_site_result + get '/logout_id_site' => actions['id_site_logout#new'], as: :logout_id_site + end end end end diff --git a/spec/dummy/app/views/layouts/application.html.erb b/spec/dummy/app/views/layouts/application.html.erb index 593a778..781bcfa 100644 --- a/spec/dummy/app/views/layouts/application.html.erb +++ b/spec/dummy/app/views/layouts/application.html.erb @@ -8,6 +8,11 @@
+ <% if signed_in? and Stormpath::Rails.config.web.id_site.enabled %> +Logged in as: <%= current_account.given_name %>
+ <%= link_to "Log out", logout_id_site_path %> + <% end %> + <%= yield %> diff --git a/spec/factories.rb b/spec/factories.rb index ba421d8..9e3d613 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -1,6 +1,6 @@ FactoryGirl.define do factory :account, class: Stormpath::Resource::Account do - sequence(:email) { |n| "dev#{n}@example.com" } + sequence(:email) { |n| "dev-#{n}-#{Faker::Lorem.word}@example.com" } password 'Password1337' given_name { Faker::Name.first_name } surname { Faker::Name.last_name } diff --git a/spec/requests/id_site_login/get_spec.rb b/spec/requests/id_site_login/get_spec.rb new file mode 100644 index 0000000..0b494fc --- /dev/null +++ b/spec/requests/id_site_login/get_spec.rb @@ -0,0 +1,116 @@ +require 'spec_helper' + +describe 'IdSiteLogin GET', type: :request, vcr: true do + let(:application) { Stormpath::Rails::Client.application } + Timecop.freeze(Time.zone.now) do + let(:time) { Time.zone.now.to_i } + end + let(:cb_uri) { '/id_site_result' } + let(:path) { '' } + let(:tenant_name) { application.tenant.name } + let(:tenant_domain) { "https://#{tenant_name}.id.stormpath.io" } + let(:api_key_secret) { ENV['STORMPATH_API_KEY_SECRET'] } + let(:aud) { ENV['STORMPATH_API_KEY_ID'] } + let(:account) { application.accounts.create(account_attrs) } + let(:account_attrs) { FactoryGirl.attributes_for(:account) } + let(:jwt_response) do + JWT.encode( + { + 'iss' => tenant_domain, + 'sub' => account.href, + 'aud' => aud, + 'exp' => time + 1.minute.to_i, + 'iat' => time, + 'jti' => 'JX5HSMmEAevFBKJx4FfC3', + 'irt' => '5fbb73e7-f81b-41f2-8031-b08750da6298', + 'state' => '', + 'isNewSub' => false, + 'status' => 'AUTHENTICATED', + 'cb_uri' => 'http://localhost:3000/id_site_result' + }, + api_key_secret, + 'HS256' + ) + end + + before do + allow(web_config.id_site).to receive(:enabled).and_return(true) + Rails.application.reload_routes! + end + + after do + account.delete if account + allow(web_config.id_site).to receive(:enabled).and_return(false) + Rails.application.reload_routes! + end + + describe 'HTTP_ACCEPT=text/html' do + context 'successfull login' do + it 'should redirect' do + get '/id_site_result', jwtResponse: jwt_response + expect(response).to redirect_to('/') + expect(response.status).to eq(302) + end + end + + context 'invalid jwt' do + describe 'expired' do + let(:time) { Time.zone.now.to_i - 10.minutes } + + it 'should render flash error' do + get '/id_site_result', jwtResponse: jwt_response + expect(controller).to set_flash[:error].now + end + end + + describe 'bad signature' do + let(:api_key_secret) { 'badapikeysecret' } + + it 'should render flash error' do + get '/id_site_result', jwtResponse: jwt_response + expect(controller).to set_flash[:error].now + end + end + end + end + + describe 'application/json' do + let(:headers) do + { + 'ACCEPT' => 'application/json' + } + end + + context 'successfull login' do + it 'should respond with ok' do + get '/id_site_result', { jwtResponse: jwt_response }, headers + expect(response.status).to eq(200) + end + + it 'should respond with the logged in account' do + get '/id_site_result', { jwtResponse: jwt_response }, headers + expect(response.body).to include('account') + end + end + + context 'invalid jwt' do + describe 'expired' do + let(:time) { Time.zone.now.to_i - 10.minutes } + + it 'should raise error' do + get '/id_site_result', { jwtResponse: jwt_response }, headers + expect(response.body).to include('message', 'Token is invalid') + end + end + + describe 'bad signature' do + let(:api_key_secret) { 'badapikeysecret' } + + it 'should render flash error' do + get '/id_site_result', { jwtResponse: jwt_response }, headers + expect(response.body).to include('message', 'Signature verification raised') + end + end + end + end +end diff --git a/spec/requests/login/get_spec.rb b/spec/requests/login/get_spec.rb index 0ae2877..1b03425 100644 --- a/spec/requests/login/get_spec.rb +++ b/spec/requests/login/get_spec.rb @@ -49,9 +49,21 @@ def json_login_get xit 'login should show account stores' do end - xit 'if id site enabled should redirect' do - json_login_get - expect(response.status).to eq(400) + describe 'if id site is enabled' do + before do + allow(web_config.id_site).to receive(:enabled).and_return(true) + Rails.application.reload_routes! + end + + after do + allow(web_config.id_site).to receive(:enabled).and_return(false) + Rails.application.reload_routes! + end + + it 'should redirect' do + json_login_get + expect(response.status).to eq(302) + end end end diff --git a/spec/requests/registration/get_spec.rb b/spec/requests/registration/get_spec.rb index bbcb43d..642164f 100644 --- a/spec/requests/registration/get_spec.rb +++ b/spec/requests/registration/get_spec.rb @@ -64,9 +64,21 @@ def json_registration_get xit 'register should show account stores' do end - xit 'if id site enabled should redirect' do - json_registration_get - expect(response.status).to eq(400) + describe 'if id site is enabled' do + before do + allow(web_config.id_site).to receive(:enabled).and_return(true) + Rails.application.reload_routes! + end + + after do + allow(web_config.id_site).to receive(:enabled).and_return(false) + Rails.application.reload_routes! + end + + it 'should redirect' do + json_registration_get + expect(response.status).to eq(302) + end end end diff --git a/spec/services/controller_authenticaton_spec.rb b/spec/services/controller_authenticaton_spec.rb index a4644e2..c5eeb4d 100644 --- a/spec/services/controller_authenticaton_spec.rb +++ b/spec/services/controller_authenticaton_spec.rb @@ -536,7 +536,7 @@ end describe 'with un encoded api key and secret' do - let(:credentials) { "#{api_key.id}:#{api_key.secret}" } + let(:credentials) { 'unencodedapikeyid:unencodedapikeysecret' } it 'raises an UnauthenticatedRequest error' do expect do diff --git a/spec/services/delete_refresh_token_spec.rb b/spec/services/delete_refresh_token_spec.rb index f2ee787..0a22023 100644 --- a/spec/services/delete_refresh_token_spec.rb +++ b/spec/services/delete_refresh_token_spec.rb @@ -24,7 +24,7 @@ after { delete_test_account } - it 'deletes the access token' do + it 'deletes the refresh token' do expect do Stormpath::Rails::DeleteRefreshToken.new(refresh_token).call end.to change { account.refresh_tokens.count }.from(1).to(0) diff --git a/spec/services/token_and_cookies_cleaner_spec.rb b/spec/services/token_and_cookies_cleaner_spec.rb new file mode 100644 index 0000000..dd6e608 --- /dev/null +++ b/spec/services/token_and_cookies_cleaner_spec.rb @@ -0,0 +1,45 @@ +require 'spec_helper' + +describe Stormpath::Rails::TokenAndCookiesCleaner, vcr: true, type: :service do + let(:account) { create_test_account } + + let(:password_grant_request) do + Stormpath::Oauth::PasswordGrantRequest.new(account.email, 'Password1337') + end + + let(:application) { Stormpath::Rails::Client.application } + + let(:access_token_authentication_result) do + application.authenticate_oauth(password_grant_request) + end + + let(:access_token) { access_token_authentication_result.access_token } + + let(:refresh_token) { access_token_authentication_result.refresh_token } + + let(:mocked_cookies_session) do + { + 'access_token' => access_token, + 'refresh_token' => refresh_token + } + end + + before do + account + access_token_authentication_result + end + + after { delete_test_account } + + it 'deletes the access token' do + expect do + Stormpath::Rails::TokenAndCookiesCleaner.new(mocked_cookies_session).remove + end.to change { account.access_tokens.count }.from(1).to(0) + end + + it 'deletes the refresh token' do + expect do + Stormpath::Rails::TokenAndCookiesCleaner.new(mocked_cookies_session).remove + end.to change { account.refresh_tokens.count }.from(1).to(0) + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 0b5d048..01115dd 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -55,6 +55,7 @@ config.include Stormpath::Testing::Helpers, type: :request config.include Stormpath::Testing::Helpers, type: :feature config.include Stormpath::Testing::Helpers, type: :service + config.include Rails.application.routes.url_helpers, type: :service config.include MatchJson::Matchers config.include Capybara::DSL, type: :feature config.include ConfigSpecHelpers @@ -81,3 +82,5 @@ Capybara.register_driver :rack_test do |app| Capybara::RackTest::Driver.new(app, headers: { 'HTTP_ACCEPT' => 'text/html' }) end + +Rails.application.routes.default_url_options[:host]= 'localhost:3000'