Introduce iOS authentication flow with JWT token generation

This commit is contained in:
Eugene Burmakin 2025-09-21 12:46:59 +02:00
parent d6a3200632
commit c0e756d085
5 changed files with 109 additions and 4 deletions

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -1,7 +1,8 @@
{
"webcredentials": {
"apps": [
"2A275P77DQ.app.dawarich.Dawarich"
"2A275P77DQ.app.dawarich.Dawarich",
"3DJN84WAS8.app.dawarich.Dawarich"
]
}
}

View file

@ -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