From c0e756d0853c1d492ef1be08700447c58777cfc1 Mon Sep 17 00:00:00 2001 From: Eugene Burmakin Date: Sun, 21 Sep 2025 12:46:59 +0200 Subject: [PATCH] Introduce iOS authentication flow with JWT token generation --- app/controllers/application_controller.rb | 15 ++++ app/controllers/auth/ios_controller.rb | 14 ++++ config/routes.rb | 3 + public/.well-known/apple-app-site-association | 3 +- spec/requests/authentication_spec.rb | 78 ++++++++++++++++++- 5 files changed, 109 insertions(+), 4 deletions(-) create mode 100644 app/controllers/auth/ios_controller.rb diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 29062343..96485374 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -39,6 +39,21 @@ class ApplicationController < ActionController::Base user_not_authorized end + def after_sign_in_path_for(resource) + payload = { api_key: resource.api_key, exp: 5.minutes.from_now.to_i } + + token = Subscription::EncodeJwtToken.new( + payload, ENV['AUTH_JWT_SECRET_KEY'] + ).call + + case request.headers['X-Dawarich-Client'] + when 'ios' + ios_success_path(token:) + else + super + end + end + private def set_self_hosted_status diff --git a/app/controllers/auth/ios_controller.rb b/app/controllers/auth/ios_controller.rb new file mode 100644 index 00000000..a3df4f5a --- /dev/null +++ b/app/controllers/auth/ios_controller.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module Auth + class IosController < ApplicationController + def success + render json: { + success: true, + message: 'iOS authentication successful', + token: params[:token], + redirect_url: root_url + }, status: :ok + end + end +end \ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index 1a592e5a..4424f062 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -85,6 +85,9 @@ Rails.application.routes.draw do root to: 'home#index' + # iOS mobile auth success endpoint + get 'auth/ios/success', to: 'auth/ios#success', as: :ios_success + if SELF_HOSTED devise_for :users, skip: [:registrations] as :user do diff --git a/public/.well-known/apple-app-site-association b/public/.well-known/apple-app-site-association index fbf3900a..b32ab0f3 100644 --- a/public/.well-known/apple-app-site-association +++ b/public/.well-known/apple-app-site-association @@ -1,7 +1,8 @@ { "webcredentials": { "apps": [ - "2A275P77DQ.app.dawarich.Dawarich" + "2A275P77DQ.app.dawarich.Dawarich", + "3DJN84WAS8.app.dawarich.Dawarich" ] } } diff --git a/spec/requests/authentication_spec.rb b/spec/requests/authentication_spec.rb index eab3f9a0..f98efab8 100644 --- a/spec/requests/authentication_spec.rb +++ b/spec/requests/authentication_spec.rb @@ -6,9 +6,9 @@ RSpec.describe 'Authentication', type: :request do let(:user) { create(:user, password: 'password123') } before do - stub_request(:get, "https://api.github.com/repos/Freika/dawarich/tags") - .with(headers: { 'Accept'=>'*/*', 'Accept-Encoding'=>/.*/, - 'Host'=>'api.github.com', 'User-Agent'=>/.*/}) + stub_request(:get, 'https://api.github.com/repos/Freika/dawarich/tags') + .with(headers: { 'Accept' => '*/*', 'Accept-Encoding' => /.*/, + 'Host' => 'api.github.com', 'User-Agent' => /.*/ }) .to_return(status: 200, body: '[{"name": "1.0.0"}]', headers: {}) end @@ -66,4 +66,76 @@ RSpec.describe 'Authentication', type: :request do expect(response).to redirect_to(new_user_session_path) end end + + describe 'Mobile iOS Authentication' do + it 'redirects to iOS success path when signing in with iOS client header' do + # Sign in with iOS client header + sign_in user + + # Mock the after_sign_in_path_for redirect behavior + allow_any_instance_of(ApplicationController).to receive(:after_sign_in_path_for).and_return(ios_success_path) + + # Make a request with the iOS client header + post user_session_path, params: { + user: { email: user.email, password: 'password123' } + }, headers: { 'X-Dawarich-Client' => 'ios' } + + # Should redirect to iOS success endpoint after successful login + expect(response).to redirect_to(ios_success_path) + end + + it 'returns JSON response with JWT token for iOS success endpoint' do + # Generate a test JWT token using the same service as the controller + payload = { api_key: user.api_key, exp: 5.minutes.from_now.to_i } + test_token = Subscription::EncodeJwtToken.new( + payload, ENV['AUTH_JWT_SECRET_KEY'] + ).call + + get ios_success_path, params: { token: test_token } + + expect(response).to be_successful + expect(response.content_type).to include('application/json') + + json_response = JSON.parse(response.body) + expect(json_response['success']).to be true + expect(json_response['message']).to eq('iOS authentication successful') + expect(json_response['token']).to eq(test_token) + expect(json_response['redirect_url']).to eq(root_url) + end + + it 'generates JWT token with correct payload for iOS authentication' do + # Test JWT token generation directly using the same logic as after_sign_in_path_for + payload = { api_key: user.api_key, exp: 5.minutes.from_now.to_i } + + # Create JWT token using the same service + token = Subscription::EncodeJwtToken.new( + payload, ENV['AUTH_JWT_SECRET_KEY'] + ).call + + expect(token).to be_present + + # Decode the token to verify the payload + decoded_payload = JWT.decode( + token, + ENV['AUTH_JWT_SECRET_KEY'], + true, + { algorithm: 'HS256' } + ).first + + expect(decoded_payload['api_key']).to eq(user.api_key) + expect(decoded_payload['exp']).to be_present + end + + it 'uses default path for non-iOS clients' do + sign_in user + + # Make a request without iOS client header + post user_session_path, params: { + user: { email: user.email, password: 'password123' } + } + + # Should redirect to default path (not iOS success) + expect(response).not_to redirect_to(ios_success_path) + end + end end