Skip to content

Commit df49860

Browse files
fix(auth): fix deduce_tenant logic, fix authentication recovery flow
1 parent 2a8ad8a commit df49860

4 files changed

Lines changed: 74 additions & 18 deletions

File tree

app/controllers/concerns/application_multitenancy.rb

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -25,18 +25,13 @@ def deduce_tenant
2525
# Deduces the current host. We strip any leading www from the host.
2626
# @return [String] The host, with www removed.
2727
def deduce_tenant_host
28-
if Rails.env.development?
29-
default_app_host = Application::Application.config.x.default_app_host
28+
stripped_host = request.host.downcase.start_with?('www.') ? request.host[4..] : request.host
29+
default_host = Application::Application.config.x.default_host&.gsub(/:\d+$/, '')
3030

31-
if request.host.downcase.ends_with?(default_app_host)
32-
request.host.sub(default_app_host, 'coursemology.org')
33-
else
34-
'coursemology.org'
35-
end
36-
elsif request.host.downcase.start_with?('www.')
37-
request.host[4..]
31+
if default_host && stripped_host.downcase.ends_with?(default_host)
32+
stripped_host.sub(default_host, 'coursemology.org')
3833
else
39-
request.host
34+
stripped_host
4035
end
4136
end
4237

client/app/bundles/authentication/pages/AuthenticationRedirection/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ const AuthenticationRedirection = (): JSX.Element | null => {
1111
? `${window.origin}${nextURL}`
1212
: oidcConfig.redirect_uri;
1313

14-
if (auth.isAuthenticated) <Redirectable />;
14+
if (auth.isAuthenticated) return <Redirectable />;
1515

1616
auth.signinRedirect({ redirect_uri: redirectUri });
1717

client/app/lib/components/wrappers/AuthProvider.tsx

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,18 @@ interface AuthProviderProps {
2020

2121
export const INVALID_GRANT_ERROR = 'invalid_grant';
2222

23+
const RECOVERY_ATTEMPTED_KEY = 'oidc_recovery_attempted';
24+
25+
const getCleanCallbackUrl = (): string => {
26+
const url = new URL(window.location.href);
27+
['code', 'state', 'session_state', 'iss'].forEach((p) =>
28+
url.searchParams.delete(p),
29+
);
30+
return url.toString();
31+
};
32+
2333
const onSigninCallback = (_user: User | void): void => {
34+
sessionStorage.removeItem(RECOVERY_ATTEMPTED_KEY);
2435
const url = new URL(window.location.pathname, window.location.origin);
2536
url.searchParams.set('from', 'auth');
2637
window.history.replaceState({}, document.title, url.toString());
@@ -40,18 +51,29 @@ export const AUTH_USER_MANAGER = new UserManager(oidcConfig);
4051
/**
4152
* Recovers from auth errors that occur during the signin callback, typically
4253
* caused by a stale or mismatched `state` param (e.g. a bookmarked callback
43-
* URL, or localStorage cleared between redirect and return). Clears the stale
44-
* OIDC state and returns the user to the home page to start a fresh login.
54+
* URL, or localStorage cleared between redirect and return).
55+
*
56+
* Clears the stale OIDC state and attempts to re-authenticate the user.
57+
* If re-authentication fails again, returns the user to the home page to start a fresh login.
4558
*/
4659
const AuthErrorRecovery = (): null => {
47-
const { error, clearStaleState } = useAuth();
60+
const { error, clearStaleState, signinRedirect } = useAuth();
4861

4962
useEffect(() => {
5063
if (error?.source !== 'signinCallback') return;
51-
clearStaleState().finally(() =>
52-
window.location.replace(window.location.origin),
53-
);
54-
}, [error, clearStaleState]);
64+
65+
const alreadyAttempted = sessionStorage.getItem(RECOVERY_ATTEMPTED_KEY);
66+
67+
clearStaleState().finally(() => {
68+
if (alreadyAttempted) {
69+
sessionStorage.removeItem(RECOVERY_ATTEMPTED_KEY);
70+
window.location.replace(window.location.origin);
71+
} else {
72+
sessionStorage.setItem(RECOVERY_ATTEMPTED_KEY, '1');
73+
signinRedirect({ redirect_uri: getCleanCallbackUrl() });
74+
}
75+
});
76+
}, [error, clearStaleState, signinRedirect]);
5577

5678
return null;
5779
};

spec/controllers/application_controller_spec.rb

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,45 @@ def publicly_accessible?
6565
end
6666
end
6767

68+
describe 'ApplicationControllerMultitenancyConcern#deduce_tenant_host' do
69+
subject { controller.send(:deduce_tenant_host) }
70+
71+
before do
72+
allow(Application::Application.config.x).to receive(:default_host).
73+
and_return('staging.coursemology.org')
74+
end
75+
76+
context 'when the host has a www prefix' do
77+
before { @request.host = 'www.example.com' }
78+
79+
it { is_expected.to eq('example.com') }
80+
end
81+
82+
context 'when the host matches the default host' do
83+
before { @request.host = 'staging.coursemology.org' }
84+
85+
it { is_expected.to eq('coursemology.org') }
86+
end
87+
88+
context 'when the host is a subdomain of the default host' do
89+
before { @request.host = 'tenant.staging.coursemology.org' }
90+
91+
it { is_expected.to eq('tenant.coursemology.org') }
92+
end
93+
94+
context 'when the host has a www prefix and is a subdomain of the default host' do
95+
before { @request.host = 'www.tenant.staging.coursemology.org' }
96+
97+
it { is_expected.to eq('tenant.coursemology.org') }
98+
end
99+
100+
context 'when the host is unrelated to the default host' do
101+
before { @request.host = 'tenant.example.com' }
102+
103+
it { is_expected.to eq('tenant.example.com') }
104+
end
105+
end
106+
68107
describe ApplicationInternationalizationConcern do
69108
before { @old_i18n_locale = I18n.locale }
70109
after { I18n.locale = @old_i18n_locale }

0 commit comments

Comments
 (0)