FEATURE: Allow using invites when DiscourseConnect SSO is enabled (#12419)
This PR allows invitations to be used when the DiscourseConnect SSO is enabled for a site (enable_discourse_connect
) and local logins are disabled. Previously invites could not be accepted with SSO enabled simply because we did not have the code paths to handle that logic.
The invitation methods that are supported include:
- Inviting people to groups via email address
- Inviting people to topics via email address
- Using invitation links generated by the Invite Users UI in the /my/invited/pending route
The flow works like this:
- User visits an invite URL
- The normal invitation validations (redemptions/expiry) happen at that point
- We store the invite key in a secure session
- The user clicks “Accept Invitation and Continue” (see below)
- The user is redirected to /session/sso then to the SSO provider URL then back to /session/sso_login
- We retrieve the invite based on the invite key in secure session. We revalidate the invitation. We show an error to the user if it is not valid. An additional check here for invites with an email specified is to check the SSO email matches the invite email
- If the invite is OK we create the user via the normal SSO methods
- We redeem the invite and activate the user. We clear the invite key in secure session.
- If the invite had a topic we redirect the user there, otherwise we redirect to /
Note that we decided for SSO-based invites the must_approve_users
site setting is ignored, because the invite is a form of pre-approval, and because regular non-staff users cannot send out email invites or generally invite to the forum in this case.
Also deletes some group invite checks as per FIX: Allow group invites when local login is disabled. by tgxworld · Pull Request #12353 · discourse/discourse · GitHub
diff --git a/app/assets/javascripts/discourse/app/controllers/invites-show.js b/app/assets/javascripts/discourse/app/controllers/invites-show.js
index 875820d..0e640be 100644
--- a/app/assets/javascripts/discourse/app/controllers/invites-show.js
+++ b/app/assets/javascripts/discourse/app/controllers/invites-show.js
@@ -64,6 +64,11 @@ export default Controller.extend(
},
@discourseComputed
+ discourseConnectEnabled() {
+ return this.siteSettings.enable_discourse_connect;
+ },
+
+ @discourseComputed
welcomeTitle() {
return I18n.t("invites.welcome_to", {
site_name: this.siteSettings.title,
@@ -83,10 +88,17 @@ export default Controller.extend(
@discourseComputed
externalAuthsOnly() {
return (
- !this.siteSettings.enable_local_logins && this.externalAuthsEnabled
+ !this.siteSettings.enable_local_logins &&
+ this.externalAuthsEnabled &&
+ !this.siteSettings.enable_discourse_connect
);
},
+ @discourseComputed("externalAuthsOnly", "discourseConnectEnabled")
+ showSocialLoginAvailable(externalAuthsOnly, discourseConnectEnabled) {
+ return !externalAuthsOnly && !discourseConnectEnabled;
+ },
+
@discourseComputed(
"externalAuthsOnly",
"authOptions",
@@ -170,6 +182,9 @@ export default Controller.extend(
@discourseComputed
wavingHandURL: () => wavingHandURL(),
+ @discourseComputed
+ ssoPath: () => getUrl("/session/sso"),
+
actions: {
submit() {
const userFields = this.userFields;
diff --git a/app/assets/javascripts/discourse/app/templates/invites/show.hbs b/app/assets/javascripts/discourse/app/templates/invites/show.hbs
index d4a0f5a..9a45776 100644
--- a/app/assets/javascripts/discourse/app/templates/invites/show.hbs
+++ b/app/assets/javascripts/discourse/app/templates/invites/show.hbs
@@ -21,15 +21,16 @@
<p>{{user-info user=invitedBy}}</p>
{{#unless isInviteLink}}
- <p>
+ <p class="email-message">
{{html-safe yourEmailMessage}}
- {{#unless externalAuthsOnly}}
+ {{#if showSocialLoginAvailable}}
{{i18n "invites.social_login_available"}}
- {{/unless}}
+ {{/if}}
</p>
{{/unless}}
{{#if externalAuthsOnly}}
+ {{! authOptions are present once the user has followed the OmniAuth flow (e.g. twitter/google/etc) }}
{{#if authOptions}}
{{#unless isInviteLink}}
{{input-tip validation=emailValidation id="account-email-validation"}}
@@ -39,6 +40,12 @@
{{/if}}
{{/if}}
+ {{#if discourseConnectEnabled}}
+ <a class="btn btn-primary discourse-connect raw-link" href={{ssoPath}}>
+ {{i18n "continue"}}
+ </a>
+ {{/if}}
+
{{#if shouldDisplayForm}}
<form>
{{#if isInviteLink}}
diff --git a/app/assets/javascripts/discourse/tests/acceptance/invite-accept-test.js b/app/assets/javascripts/discourse/tests/acceptance/invite-accept-test.js
index 1265053..472e9cb 100644
--- a/app/assets/javascripts/discourse/tests/acceptance/invite-accept-test.js
+++ b/app/assets/javascripts/discourse/tests/acceptance/invite-accept-test.js
@@ -170,6 +170,53 @@ acceptance("Invite accept when local login is disabled", function (needs) {
});
});
+acceptance(
+ "Invite accept when DiscourseConnect SSO is enabled and local login is disabled",
+ function (needs) {
+ needs.settings({
+ enable_local_logins: false,
+ enable_discourse_connect: true,
+ });
+
+ test("invite link", async function (assert) {
+ preloadInvite({ link: true });
+
+ await visit("/invites/myvalidinvitetoken");
+
+ assert.ok(
+ !exists(".btn-social.facebook"),
+ "does not show Facebook login button"
+ );
+ assert.ok(!exists("form"), "does not display the form");
+ assert.ok(
+ !exists(".email-message"),
+ "does not show the email message with the prefilled email"
+ );
+ assert.ok(exists(".discourse-connect"), "shows the Continue button");
+ });
+
+ test("email invite link", async function (assert) {
+ preloadInvite();
+
+ await visit("/invites/myvalidinvitetoken");
+
+ assert.ok(
+ !exists(".btn-social.facebook"),
+ "does not show Facebook login button"
+ );
+ assert.ok(!exists("form"), "does not display the form");
+ assert.ok(
+ exists(".email-message"),
+ "shows the email message with the prefilled email"
+ );
+ assert.ok(exists(".discourse-connect"), "shows the Continue button");
+ assert.ok(
+ queryAll(".email-message").text().includes("foobar@example.com")
+ );
+ });
+ }
+);
+
acceptance("Invite link with authentication data", function (needs) {
needs.settings({ enable_local_logins: false });
diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb
index 8d2beca..0d39f9a 100644
--- a/app/controllers/groups_controller.rb
+++ b/app/controllers/groups_controller.rb
@@ -336,14 +336,6 @@ class GroupsController < ApplicationController
raise Discourse::InvalidParameters.new(I18n.t("groups.errors.usernames_or_emails_required"))
end
- if emails.any?
- if SiteSetting.enable_discourse_connect?
- raise Discourse::InvalidParameters.new(I18n.t("groups.errors.no_invites_with_discourse_connect"))
- elsif !SiteSetting.enable_local_logins?
- raise Discourse::InvalidParameters.new(I18n.t("groups.errors.no_invites_without_local_logins"))
- end
- end
-
if users.length > ADD_MEMBERS_LIMIT
return render_json_error(
I18n.t("groups.errors.adding_too_many_users", count: ADD_MEMBERS_LIMIT)
diff --git a/app/controllers/invites_controller.rb b/app/controllers/invites_controller.rb
index 8843efe..98210b7 100644
--- a/app/controllers/invites_controller.rb
+++ b/app/controllers/invites_controller.rb
@@ -16,7 +16,7 @@ class InvitesController < ApplicationController
expires_now
invite = Invite.find_by(invite_key: params[:id])
- if invite.present? && !invite.expired? && !invite.redeemed?
+ if invite.present? && invite.redeemable?
email = Email.obfuscate(invite.email)
# Show email if the user already authenticated their email
@@ -34,14 +34,16 @@ class InvitesController < ApplicationController
is_invite_link: invite.is_invite_link?
))
+ secure_session["invite-key"] = invite.invite_key
+
render layout: 'application'
else
- flash.now[:error] = if invite&.expired?
- I18n.t('invite.expired', base_url: Discourse.base_url)
- elsif invite&.redeemed?
- I18n.t('invite.not_found_template', site_name: SiteSetting.title, base_url: Discourse.base_url)
- else
+ flash.now[:error] = if invite.blank?
I18n.t('invite.not_found', base_url: Discourse.base_url)
+ elsif invite.redeemed?
+ I18n.t('invite.not_found_template', site_name: SiteSetting.title, base_url: Discourse.base_url)
+ elsif invite.expired?
+ I18n.t('invite.expired', base_url: Discourse.base_url)
end
render layout: 'no_ember'
@@ -165,6 +167,8 @@ class InvitesController < ApplicationController
render json: success_json
end
+ # For DiscourseConnect SSO, all invite acceptance is done
+ # via the SessionController#sso_login route
def perform_accept_invitation
params.require(:id)
params.permit(:email, :username, :name, :password, :timezone, user_custom_fields: {})
@@ -190,7 +194,7 @@ class InvitesController < ApplicationController
invite.email
end
- user = invite.redeem(attrs)
+ user = invite.redeem(**attrs)
rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotSaved => e
[... diff too long, it was truncated ...]
GitHub sha: 355d51af