about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--.env.production.sample55
-rw-r--r--Gemfile5
-rw-r--r--Gemfile.lock16
-rw-r--r--Vagrantfile1
-rw-r--r--app/controllers/about_controller.rb2
-rw-r--r--app/controllers/admin/settings_controller.rb2
-rw-r--r--app/controllers/api/v1/accounts/relationships_controller.rb2
-rw-r--r--app/controllers/auth/confirmations_controller.rb23
-rw-r--r--app/controllers/auth/omniauth_callbacks_controller.rb33
-rw-r--r--app/javascript/mastodon/containers/timeline_container.js12
-rw-r--r--app/javascript/mastodon/features/standalone/community_timeline/index.js74
-rw-r--r--app/javascript/mastodon/locales/defaultMessages.json9
-rw-r--r--app/javascript/styles/mastodon/forms.scss18
-rw-r--r--app/mailers/notification_mailer.rb6
-rw-r--r--app/models/concerns/omniauthable.rb81
-rw-r--r--app/models/form/admin_settings.rb2
-rw-r--r--app/models/identity.rb22
-rw-r--r--app/models/user.rb4
-rw-r--r--app/validators/status_length_validator.rb2
-rw-r--r--app/views/admin/settings/edit.html.haml3
-rw-r--r--app/views/auth/confirmations/finish_signup.html.haml14
-rw-r--r--app/views/auth/sessions/new.html.haml9
-rw-r--r--config/i18n-tasks.yml1
-rw-r--r--config/initializers/devise.rb27
-rw-r--r--config/initializers/omniauth.rb59
-rw-r--r--config/locales/en.yml8
-rw-r--r--config/locales/fr.yml2
-rw-r--r--config/locales/pl.yml3
-rw-r--r--config/locales/pt-BR.yml3
-rw-r--r--config/routes.rb2
-rw-r--r--config/settings.yml1
-rw-r--r--db/migrate/20180204034416_create_identities.rb11
-rw-r--r--db/schema.rb15
-rw-r--r--spec/fabricators/identity_fabricator.rb5
-rw-r--r--spec/models/identity_spec.rb5
35 files changed, 506 insertions, 31 deletions
diff --git a/.env.production.sample b/.env.production.sample
index cf1654e99..ca2e29a07 100644
--- a/.env.production.sample
+++ b/.env.production.sample
@@ -13,7 +13,7 @@ DB_PORT=5432
 # Federation
 # Note: Changing LOCAL_DOMAIN at a later time will cause unwanted side effects, including breaking all existing federation.
 # LOCAL_DOMAIN should *NOT* contain the protocol part of the domain e.g https://example.com.
-LOCAL_DOMAIN=example.com 
+LOCAL_DOMAIN=example.com
 
 # Changing LOCAL_HTTPS in production is no longer supported. (Mastodon will always serve https:// links)
 
@@ -58,7 +58,7 @@ VAPID_PUBLIC_KEY=
 # E-mail configuration
 # Note: Mailgun and SparkPost (https://sparkpo.st/smtp) each have good free tiers
 # If you want to use an SMTP server without authentication (e.g local Postfix relay)
-# then set SMTP_AUTH_METHOD and SMTP_OPENSSL_VERIFY_MODE to 'none' and 
+# then set SMTP_AUTH_METHOD and SMTP_OPENSSL_VERIFY_MODE to 'none' and
 # *comment* SMTP_LOGIN and SMTP_PASSWORD (leaving them blank is not enough).
 SMTP_SERVER=smtp.mailgun.org
 SMTP_PORT=587
@@ -135,6 +135,55 @@ STREAMING_CLUSTER_NUM=1
 # If you use Docker, you may want to assign UID/GID manually.
 # UID=1000
 # GID=1000
-
+ 
 # Maximum allowed character count
 # MAX_TOOT_CHARS=500
+
+# PAM authentication (optional)
+# PAM_ENABLED=true
+# Suffix for email address generation (nil by default)
+# PAM_DEFAULT_SUFFIX=pam
+# Name of the pam service (pam "auth" section is evaluated)
+# PAM_DEFAULT_SERVICE=rpam
+# Name of the pam service used for checking if an user can register (pam "account" section is evaluated)
+# PAM_CONTROLLED_SERVICE=rpam
+
+# Optional CAS authentication (cf. omniauth-cas) :
+# CAS_ENABLED=true
+# CAS_URL=https://sso.myserver.com/
+# CAS_HOST=sso.myserver.com/
+# CAS_PORT=443
+# CAS_SSL=true
+# CAS_VALIDATE_URL=
+# CAS_CALLBACK_URL=
+# CAS_LOGOUT_URL=
+# CAS_LOGIN_URL=
+# CAS_UID_FIELD='user'
+# CAS_CA_PATH=
+# CAS_DISABLE_SSL_VERIFICATION=false
+# CAS_UID_KEY='user'
+# CAS_NAME_KEY='name'
+# CAS_EMAIL_KEY='email'
+# CAS_NICKNAME_KEY='nickname'
+# CAS_FIRST_NAME_KEY='firstname'
+# CAS_LAST_NAME_KEY='lastname'
+# CAS_LOCATION_KEY='location'
+# CAS_IMAGE_KEY='image'
+# CAS_PHONE_KEY='phone'
+
+# Optional SAML authentication (cf. omniauth-saml)
+# SAML_ENABLED=true
+# SAML_ACS_URL=
+# SAML_ISSUER=http://localhost:3000/auth/auth/saml/metadata
+# SAML_IDP_SSO_TARGET_URL=https://idp.testshib.org/idp/profile/SAML2/Redirect/SSO
+# SAML_IDP_CERT=
+# SAML_IDP_CERT_FINGERPRINT=
+# SAML_NAME_IDENTIFIER_FORMAT=
+# SAML_CERT=
+# SAML_PRIVATE_KEY=
+# SAML_SECURITY_WANT_ASSERTION_SIGNED=true
+# SAML_SECURITY_WANT_ASSERTION_ENCRYPTED=true
+# SAML_ATTRIBUTES_STATEMENTS_UID="urn:oid:0.9.2342.19200300.100.1.1"
+# SAML_ATTRIBUTES_STATEMENTS_EMAIL="urn:oid:1.3.6.1.4.1.5923.1.1.1.6"
+# SAML_ATTRIBUTES_STATEMENTS_FULL_NAME="urn:oid:2.5.4.42"
+# SAML_UID_ATTRIBUTE="urn:oid:0.9.2342.19200300.100.1.1"
diff --git a/Gemfile b/Gemfile
index a610d0a79..1d128d657 100644
--- a/Gemfile
+++ b/Gemfile
@@ -32,7 +32,10 @@ gem 'cld3', '~> 3.2.0'
 gem 'devise', '~> 4.4'
 gem 'devise-two-factor', '~> 3.0'
 
-gem 'devise_pam_authenticatable2', '~> 8.0'
+gem 'devise_pam_authenticatable2', '~> 8.0', install_if: -> { ENV['PAM_ENABLED'] == 'true' }
+gem 'omniauth-cas', '~> 1.1', install_if: -> { ENV['CAS_ENABLED'] == 'true' }
+gem 'omniauth-saml', '~> 1.8', install_if: -> { ENV['SAML_ENABLED'] == 'true' }
+gem 'omniauth', '~> 1.2'
 
 gem 'doorkeeper', '~> 4.2'
 gem 'fast_blank', '~> 1.0'
diff --git a/Gemfile.lock b/Gemfile.lock
index d8a853b04..3a65f35a5 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -201,6 +201,7 @@ GEM
     hamster (3.0.0)
       concurrent-ruby (~> 1.0)
     hashdiff (0.3.7)
+    hashie (3.5.7)
     highline (1.7.10)
     hiredis (0.6.1)
     hkdf (0.3.0)
@@ -304,6 +305,16 @@ GEM
       sidekiq (>= 3.5.0)
       statsd-ruby (~> 1.2.0)
     oj (3.3.10)
+    omniauth (1.8.1)
+      hashie (>= 3.4.6, < 3.6.0)
+      rack (>= 1.6.2, < 3)
+    omniauth-cas (1.1.1)
+      addressable (~> 2.3)
+      nokogiri (~> 1.5)
+      omniauth (~> 1.2)
+    omniauth-saml (1.9.0)
+      omniauth (~> 1.3, >= 1.3.2)
+      ruby-saml (~> 1.4, >= 1.4.3)
     orm_adapter (0.5.0)
     ostatus2 (2.0.3)
       addressable (~> 2.5)
@@ -456,6 +467,8 @@ GEM
       unicode-display_width (~> 1.0, >= 1.0.1)
     ruby-oembed (0.12.0)
     ruby-progressbar (1.9.0)
+    ruby-saml (1.6.1)
+      nokogiri (>= 1.5.10)
     rufus-scheduler (3.4.2)
       et-orbi (~> 1.0)
     safe_yaml (1.0.4)
@@ -607,6 +620,9 @@ DEPENDENCIES
   nokogiri (~> 1.8)
   nsa (~> 0.2)
   oj (~> 3.3)
+  omniauth (~> 1.2)
+  omniauth-cas (~> 1.1)
+  omniauth-saml (~> 1.8)
   ostatus2 (~> 2.0)
   ox (~> 2.8)
   paperclip (~> 5.1)
diff --git a/Vagrantfile b/Vagrantfile
index 52f6c0f29..e52ec556b 100644
--- a/Vagrantfile
+++ b/Vagrantfile
@@ -39,6 +39,7 @@ sudo apt-get install \
   libidn11-dev \
   libprotobuf-dev \
   libreadline-dev \
+  libpam0g-dev \
   -y
 
 # Install rvm
diff --git a/app/controllers/about_controller.rb b/app/controllers/about_controller.rb
index 8785df14e..7b46b2228 100644
--- a/app/controllers/about_controller.rb
+++ b/app/controllers/about_controller.rb
@@ -36,7 +36,7 @@ class AboutController < ApplicationController
 
   def initial_state_params
     {
-      settings: {},
+      settings: { known_fediverse: Setting.show_known_fediverse_at_about_page },
       token: current_session&.token,
     }
   end
diff --git a/app/controllers/admin/settings_controller.rb b/app/controllers/admin/settings_controller.rb
index 487282dc3..a6214dc3f 100644
--- a/app/controllers/admin/settings_controller.rb
+++ b/app/controllers/admin/settings_controller.rb
@@ -19,6 +19,7 @@ module Admin
       min_invite_role
       activity_api_enabled
       peers_api_enabled
+      show_known_fediverse_at_about_page
     ).freeze
 
     BOOLEAN_SETTINGS = %w(
@@ -28,6 +29,7 @@ module Admin
       show_staff_badge
       activity_api_enabled
       peers_api_enabled
+      show_known_fediverse_at_about_page
     ).freeze
 
     UPLOAD_SETTINGS = %w(
diff --git a/app/controllers/api/v1/accounts/relationships_controller.rb b/app/controllers/api/v1/accounts/relationships_controller.rb
index 91a942d75..6cc3da498 100644
--- a/app/controllers/api/v1/accounts/relationships_controller.rb
+++ b/app/controllers/api/v1/accounts/relationships_controller.rb
@@ -10,7 +10,7 @@ class Api::V1::Accounts::RelationshipsController < Api::BaseController
     accounts = Account.where(id: account_ids).select('id')
     # .where doesn't guarantee that our results are in the same order
     # we requested them, so return the "right" order to the requestor.
-    @accounts = accounts.index_by(&:id).values_at(*account_ids)
+    @accounts = accounts.index_by(&:id).values_at(*account_ids).compact
     render json: @accounts, each_serializer: REST::RelationshipSerializer, relationships: relationships
   end
 
diff --git a/app/controllers/auth/confirmations_controller.rb b/app/controllers/auth/confirmations_controller.rb
index 72b8e9dd8..f3e0ae257 100644
--- a/app/controllers/auth/confirmations_controller.rb
+++ b/app/controllers/auth/confirmations_controller.rb
@@ -3,6 +3,7 @@
 class Auth::ConfirmationsController < Devise::ConfirmationsController
   layout 'auth'
 
+  before_action :set_user, only: [:finish_signup]
   before_action :set_pack
 
   private
@@ -10,4 +11,26 @@ class Auth::ConfirmationsController < Devise::ConfirmationsController
   def set_pack
     use_pack 'auth'
   end
+
+  # GET/PATCH /users/:id/finish_signup
+  def finish_signup
+    return unless request.patch? && params[:user]
+    if @user.update(user_params)
+      @user.skip_reconfirmation!
+      sign_in(@user, bypass: true)
+      redirect_to root_path, notice: I18n.t('devise.confirmations.send_instructions')
+    else
+      @show_errors = true
+    end
+  end
+
+  private
+
+  def set_user
+    @user = current_user
+  end
+
+  def user_params
+    params.require(:user).permit(:email)
+  end
 end
diff --git a/app/controllers/auth/omniauth_callbacks_controller.rb b/app/controllers/auth/omniauth_callbacks_controller.rb
new file mode 100644
index 000000000..bbf63bed3
--- /dev/null
+++ b/app/controllers/auth/omniauth_callbacks_controller.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+class Auth::OmniauthCallbacksController < Devise::OmniauthCallbacksController
+  skip_before_action :verify_authenticity_token
+
+  def self.provides_callback_for(provider)
+    provider_id = provider.to_s.chomp '_oauth2'
+
+    define_method provider do
+      @user = User.find_for_oauth(request.env['omniauth.auth'], current_user)
+
+      if @user.persisted?
+        sign_in_and_redirect @user, event: :authentication
+        set_flash_message(:notice, :success, kind: provider_id.capitalize) if is_navigational_format?
+      else
+        session["devise.#{provider}_data"] = request.env['omniauth.auth']
+        redirect_to new_user_registration_url
+      end
+    end
+  end
+
+  Devise.omniauth_configs.each_key do |provider|
+    provides_callback_for provider
+  end
+
+  def after_sign_in_path_for(resource)
+    if resource.email_verified?
+      root_path
+    else
+      finish_signup_path
+    end
+  end
+end
diff --git a/app/javascript/mastodon/containers/timeline_container.js b/app/javascript/mastodon/containers/timeline_container.js
index e84c921ee..8719bb5c9 100644
--- a/app/javascript/mastodon/containers/timeline_container.js
+++ b/app/javascript/mastodon/containers/timeline_container.js
@@ -6,6 +6,7 @@ import { hydrateStore } from '../actions/store';
 import { IntlProvider, addLocaleData } from 'react-intl';
 import { getLocale } from '../locales';
 import PublicTimeline from '../features/standalone/public_timeline';
+import CommunityTimeline from '../features/standalone/community_timeline';
 import HashtagTimeline from '../features/standalone/hashtag_timeline';
 import initialState from '../initial_state';
 
@@ -23,17 +24,24 @@ export default class TimelineContainer extends React.PureComponent {
   static propTypes = {
     locale: PropTypes.string.isRequired,
     hashtag: PropTypes.string,
+    showPublicTimeline: PropTypes.bool.isRequired,
+  };
+
+  static defaultProps = {
+    showPublicTimeline: initialState.settings.known_fediverse,
   };
 
   render () {
-    const { locale, hashtag } = this.props;
+    const { locale, hashtag, showPublicTimeline } = this.props;
 
     let timeline;
 
     if (hashtag) {
       timeline = <HashtagTimeline hashtag={hashtag} />;
-    } else {
+    } else if (showPublicTimeline) {
       timeline = <PublicTimeline />;
+    } else {
+      timeline = <CommunityTimeline />;
     }
 
     return (
diff --git a/app/javascript/mastodon/features/standalone/community_timeline/index.js b/app/javascript/mastodon/features/standalone/community_timeline/index.js
new file mode 100644
index 000000000..51e50e1f5
--- /dev/null
+++ b/app/javascript/mastodon/features/standalone/community_timeline/index.js
@@ -0,0 +1,74 @@
+import React from 'react';
+import { connect } from 'react-redux';
+import PropTypes from 'prop-types';
+import StatusListContainer from '../../ui/containers/status_list_container';
+import {
+  refreshCommunityTimeline,
+  expandCommunityTimeline,
+} from '../../../actions/timelines';
+import Column from '../../../components/column';
+import ColumnHeader from '../../../components/column_header';
+import { defineMessages, injectIntl } from 'react-intl';
+import { connectCommunityStream } from '../../../actions/streaming';
+
+const messages = defineMessages({
+  title: { id: 'standalone.public_title', defaultMessage: 'A look inside...' },
+});
+
+@connect()
+@injectIntl
+export default class CommunityTimeline extends React.PureComponent {
+
+  static propTypes = {
+    dispatch: PropTypes.func.isRequired,
+    intl: PropTypes.object.isRequired,
+  };
+
+  handleHeaderClick = () => {
+    this.column.scrollTop();
+  }
+
+  setRef = c => {
+    this.column = c;
+  }
+
+  componentDidMount () {
+    const { dispatch } = this.props;
+
+    dispatch(refreshCommunityTimeline());
+    this.disconnect = dispatch(connectCommunityStream());
+  }
+
+  componentWillUnmount () {
+    if (this.disconnect) {
+      this.disconnect();
+      this.disconnect = null;
+    }
+  }
+
+  handleLoadMore = () => {
+    this.props.dispatch(expandCommunityTimeline());
+  }
+
+  render () {
+    const { intl } = this.props;
+
+    return (
+      <Column ref={this.setRef}>
+        <ColumnHeader
+          icon='users'
+          title={intl.formatMessage(messages.title)}
+          onClick={this.handleHeaderClick}
+        />
+
+        <StatusListContainer
+          timelineId='community'
+          loadMore={this.handleLoadMore}
+          scrollKey='standalone_public_timeline'
+          trackScroll={false}
+        />
+      </Column>
+    );
+  }
+
+}
diff --git a/app/javascript/mastodon/locales/defaultMessages.json b/app/javascript/mastodon/locales/defaultMessages.json
index a15da83ab..a27f17b42 100644
--- a/app/javascript/mastodon/locales/defaultMessages.json
+++ b/app/javascript/mastodon/locales/defaultMessages.json
@@ -1237,6 +1237,15 @@
         "id": "standalone.public_title"
       }
     ],
+    "path": "app/javascript/mastodon/features/standalone/community_timeline/index.json"
+  },
+  {
+    "descriptors": [
+      {
+        "defaultMessage": "A look inside...",
+        "id": "standalone.public_title"
+      }
+    ],
     "path": "app/javascript/mastodon/features/standalone/public_timeline/index.json"
   },
   {
diff --git a/app/javascript/styles/mastodon/forms.scss b/app/javascript/styles/mastodon/forms.scss
index 2bef53cff..dec7d2284 100644
--- a/app/javascript/styles/mastodon/forms.scss
+++ b/app/javascript/styles/mastodon/forms.scss
@@ -568,3 +568,21 @@ code {
     margin-bottom: 4px;
   }
 }
+
+.alternative-login {
+  margin-top: 20px;
+  margin-bottom: 20px;
+
+  h4 {
+    font-size: 16px;
+    color: $ui-base-lighter-color;
+    text-align: center;
+    margin-bottom: 20px;
+    border: 0;
+    padding: 0;
+  }
+
+  .button {
+    display: block;
+  }
+}
diff --git a/app/mailers/notification_mailer.rb b/app/mailers/notification_mailer.rb
index 9fed4a636..b45844296 100644
--- a/app/mailers/notification_mailer.rb
+++ b/app/mailers/notification_mailer.rb
@@ -9,7 +9,7 @@ class NotificationMailer < ApplicationMailer
     @me     = recipient
     @status = notification.target_status
 
-    return if @me.user.disabled?
+    return if @me.user.disabled? || @status.nil?
 
     locale_for_account(@me) do
       thread_by_conversation(@status.conversation)
@@ -33,7 +33,7 @@ class NotificationMailer < ApplicationMailer
     @account = notification.from_account
     @status  = notification.target_status
 
-    return if @me.user.disabled?
+    return if @me.user.disabled? || @status.nil?
 
     locale_for_account(@me) do
       thread_by_conversation(@status.conversation)
@@ -46,7 +46,7 @@ class NotificationMailer < ApplicationMailer
     @account = notification.from_account
     @status  = notification.target_status
 
-    return if @me.user.disabled?
+    return if @me.user.disabled? || @status.nil?
 
     locale_for_account(@me) do
       thread_by_conversation(@status.conversation)
diff --git a/app/models/concerns/omniauthable.rb b/app/models/concerns/omniauthable.rb
new file mode 100644
index 000000000..a3d55108d
--- /dev/null
+++ b/app/models/concerns/omniauthable.rb
@@ -0,0 +1,81 @@
+# frozen_string_literal: true
+
+module Omniauthable
+  extend ActiveSupport::Concern
+
+  TEMP_EMAIL_PREFIX = 'change@me'
+  TEMP_EMAIL_REGEX = /\Achange@me/
+
+  included do
+    def omniauth_providers
+      Devise.omniauth_configs.keys
+    end
+
+    def email_verified?
+      email && email !~ TEMP_EMAIL_REGEX
+    end
+  end
+
+  class_methods do
+    def find_for_oauth(auth, signed_in_resource = nil)
+      # EOLE-SSO Patch
+      auth.uid = (auth.uid[0][:uid] || auth.uid[0][:user]) if auth.uid.is_a? Hashie::Array
+      identity = Identity.find_for_oauth(auth)
+
+      # If a signed_in_resource is provided it always overrides the existing user
+      # to prevent the identity being locked with accidentally created accounts.
+      # Note that this may leave zombie accounts (with no associated identity) which
+      # can be cleaned up at a later date.
+      user = signed_in_resource ? signed_in_resource : identity.user
+      user = create_for_oauth(auth) if user.nil?
+
+      if identity.user.nil?
+        identity.user = user
+        identity.save!
+      end
+
+      user
+    end
+
+    def create_for_oauth(auth)
+      # Check if the user exists with provided email if the provider gives us a
+      # verified email.  If no verified email was provided or the user already
+      # exists, we assign a temporary email and ask the user to verify it on
+      # the next step via Auth::ConfirmationsController.finish_signup
+
+      user = User.new(user_params_from_auth(auth))
+      user.account.avatar_remote_url = auth.info.image if auth.info.image =~ /\A#{URI.regexp(%w(http https))}\z/
+      user.skip_confirmation!
+      user.save!
+      user
+    end
+
+    private
+
+    def user_params_from_auth(auth)
+      email_is_verified = auth.info.email && (auth.info.verified || auth.info.verified_email)
+      email             = auth.info.email if email_is_verified && !User.exists?(email: auth.info.email)
+
+      {
+        email: email ? email : "#{TEMP_EMAIL_PREFIX}-#{auth.uid}-#{auth.provider}.com",
+        password: Devise.friendly_token[0, 20],
+        account_attributes: {
+          username: ensure_unique_username(auth.uid),
+          display_name: [auth.info.first_name, auth.info.last_name].join(' '),
+        },
+      }
+    end
+
+    def ensure_unique_username(starting_username)
+      username = starting_username
+      i        = 0
+
+      while Account.exists?(username: username)
+        i       += 1
+        username = "#{starting_username}_#{i}"
+      end
+
+      username
+    end
+  end
+end
diff --git a/app/models/form/admin_settings.rb b/app/models/form/admin_settings.rb
index dd629279c..32922e7f1 100644
--- a/app/models/form/admin_settings.rb
+++ b/app/models/form/admin_settings.rb
@@ -34,6 +34,8 @@ class Form::AdminSettings
     :activity_api_enabled=,
     :peers_api_enabled,
     :peers_api_enabled=,
+    :show_known_fediverse_at_about_page,
+    :show_known_fediverse_at_about_page=,
     to: Setting
   )
 end
diff --git a/app/models/identity.rb b/app/models/identity.rb
new file mode 100644
index 000000000..a5e0c09ec
--- /dev/null
+++ b/app/models/identity.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+# == Schema Information
+#
+# Table name: identities
+#
+#  id         :integer          not null, primary key
+#  user_id    :integer
+#  provider   :string           default(""), not null
+#  uid        :string           default(""), not null
+#  created_at :datetime         not null
+#  updated_at :datetime         not null
+#
+
+class Identity < ApplicationRecord
+  belongs_to :user, dependent: :destroy
+  validates :uid, presence: true, uniqueness: { scope: :provider }
+  validates :provider, presence: true
+
+  def self.find_for_oauth(auth)
+    find_or_create_by(uid: auth.uid, provider: auth.provider)
+  end
+end
diff --git a/app/models/user.rb b/app/models/user.rb
index 6ef6db915..d495fc390 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -39,6 +39,7 @@
 
 class User < ApplicationRecord
   include Settings::Extend
+  include Omniauthable
 
   ACTIVE_DURATION = 14.days
 
@@ -51,7 +52,8 @@ class User < ApplicationRecord
   devise :registerable, :recoverable, :rememberable, :trackable, :validatable,
          :confirmable
 
-  devise :pam_authenticatable
+  devise :pam_authenticatable if Devise.pam_authentication
+  devise :omniauthable
 
   belongs_to :account, inverse_of: :user
   belongs_to :invite, counter_cache: :uses, optional: true
diff --git a/app/validators/status_length_validator.rb b/app/validators/status_length_validator.rb
index 79d17742a..2db9b29f6 100644
--- a/app/validators/status_length_validator.rb
+++ b/app/validators/status_length_validator.rb
@@ -23,6 +23,8 @@ class StatusLengthValidator < ActiveModel::Validator
   end
 
   def countable_text(status)
+    return '' if status.text.nil?
+
     status.text.dup.tap do |new_text|
       new_text.gsub!(FetchLinkCardService::URL_PATTERN, 'x' * 23)
       new_text.gsub!(Account::MENTION_RE, '@\2')
diff --git a/app/views/admin/settings/edit.html.haml b/app/views/admin/settings/edit.html.haml
index 4f9115ed2..73fd5642e 100644
--- a/app/views/admin/settings/edit.html.haml
+++ b/app/views/admin/settings/edit.html.haml
@@ -19,6 +19,9 @@
     = f.input :timeline_preview, as: :boolean, wrapper: :with_label, label: t('admin.settings.timeline_preview.title'), hint: t('admin.settings.timeline_preview.desc_html')
 
   .fields-group
+    = f.input :show_known_fediverse_at_about_page, as: :boolean, wrapper: :with_label, label: t('admin.settings.show_known_fediverse_at_about_page.title'), hint: t('admin.settings.show_known_fediverse_at_about_page.desc_html')
+
+  .fields-group
     = f.input :show_staff_badge, as: :boolean, wrapper: :with_label, label: t('admin.settings.show_staff_badge.title'), hint: t('admin.settings.show_staff_badge.desc_html')
 
   .fields-group
diff --git a/app/views/auth/confirmations/finish_signup.html.haml b/app/views/auth/confirmations/finish_signup.html.haml
new file mode 100644
index 000000000..4b5161d6b
--- /dev/null
+++ b/app/views/auth/confirmations/finish_signup.html.haml
@@ -0,0 +1,14 @@
+- content_for :page_title do
+  = t('auth.confirm_email')
+
+= simple_form_for(current_user, as: 'user', url: finish_signup_path, html: { role: 'form'}) do |f|
+  - if @show_errors && current_user.errors.any?
+    #error_explanation
+      - current_user.errors.full_messages.each do |msg|
+        = msg
+        %br
+
+  = f.input :email
+
+  .actions
+    = f.submit t('auth.confirm_email'), class: 'button'
diff --git a/app/views/auth/sessions/new.html.haml b/app/views/auth/sessions/new.html.haml
index 3edb0d2d4..1c3a0b6b4 100644
--- a/app/views/auth/sessions/new.html.haml
+++ b/app/views/auth/sessions/new.html.haml
@@ -14,4 +14,13 @@
   .actions
     = f.button :button, t('auth.login'), type: :submit
 
+- if devise_mapping.omniauthable? and resource_class.omniauth_providers.any?
+  .simple_form.alternative-login
+    %h4= t('auth.or_log_in_with')
+
+    .actions
+      - resource_class.omniauth_providers.each do |provider|
+        = link_to omniauth_authorize_path(resource_name, provider), class: "button button-#{provider}" do
+          = t("auth.providers.#{provider}", default: provider.to_s.chomp("_oauth2").capitalize)
+
 .form-footer= render 'auth/shared/links'
diff --git a/config/i18n-tasks.yml b/config/i18n-tasks.yml
index 9e1b620f5..62e78556f 100644
--- a/config/i18n-tasks.yml
+++ b/config/i18n-tasks.yml
@@ -46,6 +46,7 @@ ignore_missing:
   - 'terms.body_html'
   - 'application_mailer.salutation'
   - 'errors.500'
+  - 'auth.providers.*'
 
 ignore_unused:
   - 'activemodel.errors.*'
diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb
index f2f7f1ba3..ba7ad9e6c 100644
--- a/config/initializers/devise.rb
+++ b/config/initializers/devise.rb
@@ -315,22 +315,13 @@ Devise.setup do |config|
   # so you need to do it manually. For the users scope, it would be:
   # config.omniauth_path_prefix = '/my_engine/users/auth'
 
-  # PAM: only look for email field
-  config.usernamefield = nil
-  config.emailfield = "email"
-
-  # authentication with pam possible
-  # if not enabled, all pam settings are ignored
-  #config.pam_authentication = true
-  # check if email is actually a username
-  config.check_at_sign = true
-  # suffix for email address generation (warning: without pam must provide email in the pam environment)
-  config.pam_default_suffix = "pam"
-  # name of the pam service
-  # pam "auth" section is evaluated
-  config.pam_default_service = "rpam"
-  # name of the pam service used for checking if an user can register
-  # pam "account" section is evaluated
-  # nil for allowing registration of pam names (not recommended)
-  config.pam_controlled_service = "rpam"
+  if ENV['PAM_ENABLED'] == 'true'
+    config.pam_authentication     = true
+    config.usernamefield          = nil
+    config.emailfield             = 'email'
+    config.check_at_sign          = true
+    config.pam_default_suffix     = ENV.fetch('PAM_DEFAULT_SUFFIX') { nil }
+    config.pam_default_service    = ENV.fetch('PAM_DEFAULT_SERVICE') { 'rpam' }
+    config.pam_controlled_service = ENV.fetch('PAM_CONTROLLED_SERVICE') { 'rpam' }
+  end
 end
diff --git a/config/initializers/omniauth.rb b/config/initializers/omniauth.rb
new file mode 100644
index 000000000..97f32c0a4
--- /dev/null
+++ b/config/initializers/omniauth.rb
@@ -0,0 +1,59 @@
+Rails.application.config.middleware.use OmniAuth::Builder do
+  # Vanilla omniauth stategies
+end
+
+Devise.setup do |config|
+  # Devise omniauth strategies
+
+  # CAS strategy
+  if ENV['CAS_ENABLED'] == 'true'
+    cas_options = {}
+    cas_options[:url] = ENV['CAS_URL'] if ENV['CAS_URL']
+    cas_options[:host] = ENV['CAS_HOST'] if ENV['CAS_HOST']
+    cas_options[:port] = ENV['CAS_PORT'] if ENV['CAS_PORT']
+    cas_options[:ssl] = ENV['CAS_SSL'] == 'true' if ENV['CAS_SSL']
+    cas_options[:validate_url] = ENV['CAS_VALIDATE_URL'] if ENV['CAS_VALIDATE_URL']
+    cas_options[:callback_url] = ENV['CAS_CALLBACK_URL'] if ENV['CAS_CALLBACK_URL']
+    cas_options[:logout_url] = ENV['CAS_LOGOUT_URL'] if ENV['CAS_LOGOUT_URL']
+    cas_options[:login_url] = ENV['CAS_LOGIN_URL'] if ENV['CAS_LOGIN_URL']
+    cas_options[:uid_field] = ENV['CAS_UID_FIELD'] || 'user' if ENV['CAS_UID_FIELD']
+    cas_options[:ca_path] = ENV['CAS_CA_PATH'] if ENV['CAS_CA_PATH']
+    cas_options[:disable_ssl_verification] = ENV['CAS_DISABLE_SSL_VERIFICATION'] == 'true' if ENV['CAS_DISABLE_SSL_VERIFICATION']
+    cas_options[:uid_key] = ENV['CAS_UID_KEY'] || 'user'
+    cas_options[:name_key] = ENV['CAS_NAME_KEY'] || 'name'
+    cas_options[:email_key] = ENV['CAS_EMAIL_KEY'] || 'email'
+    cas_options[:nickname_key] = ENV['CAS_NICKNAME_KEY'] || 'nickname'
+    cas_options[:first_name_key] = ENV['CAS_FIRST_NAME_KEY'] || 'firstname'
+    cas_options[:last_name_key] = ENV['CAS_LAST_NAME_KEY'] || 'lastname'
+    cas_options[:location_key] = ENV['CAS_LOCATION_KEY'] || 'location'
+    cas_options[:image_key] = ENV['CAS_IMAGE_KEY'] || 'image'
+    cas_options[:phone_key] = ENV['CAS_PHONE_KEY'] || 'phone'
+    config.omniauth :cas, cas_options
+  end
+
+  # SAML strategy
+  if ENV['SAML_ENABLED'] == 'true'
+    saml_options = {}
+    saml_options[:assertion_consumer_service_url] = ENV['SAML_ACS_URL'] if ENV['SAML_ACS_URL']
+    saml_options[:issuer] = ENV['SAML_ISSUER'] if ENV['SAML_ISSUER']
+    saml_options[:idp_sso_target_url] = ENV['SAML_IDP_SSO_TARGET_URL']  if ENV['SAML_IDP_SSO_TARGET_URL']
+    saml_options[:idp_sso_target_url_runtime_params] = ENV['SAML_IDP_SSO_TARGET_PARAMS'] if ENV['SAML_IDP_SSO_TARGET_PARAMS'] # FIXME: Should be parsable Hash
+    saml_options[:idp_cert] = ENV['SAML_IDP_CERT'] if ENV['SAML_IDP_CERT']
+    saml_options[:idp_cert_fingerprint] = ENV['SAML_IDP_CERT_FINGERPRINT'] if ENV['SAML_IDP_CERT_FINGERPRINT']
+    saml_options[:idp_cert_fingerprint_validator] = ENV['SAML_IDP_CERT_FINGERPRINT_VALIDATOR'] if ENV['SAML_IDP_CERT_FINGERPRINT_VALIDATOR'] # FIXME: Should be Lambda { |fingerprint| }
+    saml_options[:name_identifier_format] = ENV['SAML_NAME_IDENTIFIER_FORMAT'] if ENV['SAML_NAME_IDENTIFIER_FORMAT']
+    saml_options[:request_attributes] = {}
+    saml_options[:certificate] = ENV['SAML_CERT'] if ENV['SAML_CERT']
+    saml_options[:private_key] = ENV['SAML_PRIVATE_KEY'] if ENV['SAML_PRIVATE_KEY']
+    saml_options[:security] = {}
+    saml_options[:security][:want_assertions_signed] = ENV['SAML_SECURITY_WANT_ASSERTION_SIGNED'] == 'true'
+    saml_options[:security][:want_assertions_encrypted] = ENV['SAML_SECURITY_WANT_ASSERTION_ENCRYPTED'] == 'true'
+    saml_options[:attribute_statements] = {}
+    saml_options[:attribute_statements][:uid] = [ENV['SAML_ATTRIBUTES_STATEMENTS_UID']] if ENV['SAML_ATTRIBUTES_STATEMENTS_UID']
+    saml_options[:attribute_statements][:email] = [ENV['SAML_ATTRIBUTES_STATEMENTS_EMAIL']] if ENV['SAML_ATTRIBUTES_STATEMENTS_EMAIL']
+    saml_options[:attribute_statements][:full_name] = [ENV['SAML_ATTRIBUTES_STATEMENTS_FULL_NAME']] if ENV['SAML_ATTRIBUTES_STATEMENTS_FULL_NAME']
+    saml_options[:uid_attribute] = ENV['SAML_UID_ATTRIBUTE'] if ENV['SAML_UID_ATTRIBUTE']
+    config.omniauth :saml, saml_options
+  end
+
+end
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 7c3cd922b..5f9c0b3c5 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -290,6 +290,9 @@ en:
         open:
           desc_html: Allow anyone to create an account
           title: Open registration
+      show_known_fediverse_at_about_page:
+        desc_html: When toggled, it will show toots from all the known fediverse on preview. Otherwise it will only show local toots.
+        title: Show known fediverse on timeline preview
       show_staff_badge:
         desc_html: Show a staff badge on a user page
         title: Show staff badge
@@ -355,6 +358,7 @@ en:
   auth:
     agreement_html: By signing up you agree to follow <a href="%{rules_path}">the rules of the instance</a> and <a href="%{terms_path}">our terms of service</a>.
     change_password: Security
+    confirm_email: Confirm email
     delete_account: Delete account
     delete_account_html: If you wish to delete your account, you can <a href="%{path}">proceed here</a>. You will be asked for confirmation.
     didnt_get_confirmation: Didn't receive confirmation instructions?
@@ -364,6 +368,10 @@ en:
     logout: Logout
     migrate_account: Move to a different account
     migrate_account_html: If you wish to redirect this account to a different one, you can <a href="%{path}">configure it here</a>.
+    or_log_in_with: Or log in with
+    providers:
+      cas: CAS
+      saml: SAML
     register: Sign up
     resend_confirmation: Resend confirmation instructions
     reset_password: Reset password
diff --git a/config/locales/fr.yml b/config/locales/fr.yml
index b7be840a2..dcfa35802 100644
--- a/config/locales/fr.yml
+++ b/config/locales/fr.yml
@@ -355,6 +355,7 @@ fr:
   auth:
     agreement_html: En vous inscrivant, vous souscrivez <a href="%{rules_path}">aux règles de l’instance</a> et à <a href="%{terms_path}">nos conditions d’utilisation</a>.
     change_password: Sécurité
+    confirm_email: Confirmer mon adresse mail
     delete_account: Supprimer le compte
     delete_account_html: Si vous désirez supprimer votre compte, vous pouvez <a href="%{path}">cliquer ici</a>. Il vous sera demandé de confirmer cette action.
     didnt_get_confirmation: Vous n’avez pas reçu les consignes de confirmation ?
@@ -364,6 +365,7 @@ fr:
     logout: Se déconnecter
     migrate_account: Déplacer vers un compte différent
     migrate_account_html: Si vous voulez rediriger ce compte vers un autre, vous pouvez le <a href="%{path}">configurer ici</a>.
+    or_log_in_with: Ou authentifiez-vous avec
     register: S’inscrire
     resend_confirmation: Envoyer à nouveau les consignes de confirmation
     reset_password: Réinitialiser le mot de passe
diff --git a/config/locales/pl.yml b/config/locales/pl.yml
index cb5f7da44..b3b967e93 100644
--- a/config/locales/pl.yml
+++ b/config/locales/pl.yml
@@ -291,6 +291,9 @@ pl:
         open:
           desc_html: Pozwól każdemu na założenie konta
           title: Otwarta rejestracja
+      show_known_fediverse_at_about_page:
+        desc_html: Jeśli włączone, podgląd instancji będzie wyświetlał wpisy z całego Fediwersum. W innym przypadku, będą wyświetlane tylko lokalne wpisy.
+        title: Pokazuj wszystkie znane wpisy na podglądzie instancji
       show_staff_badge:
         desc_html: Pokazuj odznakę uprawnień na stronie profilu użytkownika
         title: Pokazuj odznakę administracji
diff --git a/config/locales/pt-BR.yml b/config/locales/pt-BR.yml
index de1f9a358..6b911a9a9 100644
--- a/config/locales/pt-BR.yml
+++ b/config/locales/pt-BR.yml
@@ -290,6 +290,9 @@ pt-BR:
         open:
           desc_html: Permitir que qualquer um crie uma conta
           title: Cadastro aberto
+      show_known_fediverse_at_about_page:
+        desc_html: Quando ligado, vai mostrar toots de todo o fediverso conhecido na prévia da timeline. Senão, mostra somente toots locais.
+        title: Mostrar fediverso conhecido na prévia da timeline
       show_staff_badge:
         desc_html: Mostrar uma insígnia de Equipe na página de usuário
         title: Mostrar insígnia de equipe
diff --git a/config/routes.rb b/config/routes.rb
index f45684519..8a52892f2 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -24,9 +24,11 @@ Rails.application.routes.draw do
 
   devise_scope :user do
     get '/invite/:invite_code', to: 'auth/registrations#new', as: :public_invite
+    match '/auth/finish_signup' => 'auth/confirmations#finish_signup', via: [:get, :patch], as: :finish_signup
   end
 
   devise_for :users, path: 'auth', controllers: {
+    omniauth_callbacks: 'auth/omniauth_callbacks',
     sessions:           'auth/sessions',
     registrations:      'auth/registrations',
     passwords:          'auth/passwords',
diff --git a/config/settings.yml b/config/settings.yml
index 507b7c066..592be1a8a 100644
--- a/config/settings.yml
+++ b/config/settings.yml
@@ -51,6 +51,7 @@ defaults: &defaults
   bootstrap_timeline_accounts: ''
   activity_api_enabled: true
   peers_api_enabled: true
+  show_known_fediverse_at_about_page: true
 development:
   <<: *defaults
 
diff --git a/db/migrate/20180204034416_create_identities.rb b/db/migrate/20180204034416_create_identities.rb
new file mode 100644
index 000000000..f6f5da910
--- /dev/null
+++ b/db/migrate/20180204034416_create_identities.rb
@@ -0,0 +1,11 @@
+class CreateIdentities < ActiveRecord::Migration[5.0]
+  def change
+    create_table :identities do |t|
+      t.references :user, foreign_key: { on_delete: :cascade }
+      t.string :provider, null: false, default: ''
+      t.string :uid, null: false, default: ''
+
+      t.timestamps
+    end
+  end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 816b3a030..bc44b65af 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
 #
 # It's strongly recommended that you check this file into your version control system.
 
-ActiveRecord::Schema.define(version: 20180109143959) do
+ActiveRecord::Schema.define(version: 20180204034416) do
 
   # These are extensions that must be enabled in order to support this database
   enable_extension "plpgsql"
@@ -182,6 +182,15 @@ ActiveRecord::Schema.define(version: 20180109143959) do
     t.index ["account_id"], name: "index_glitch_keyword_mutes_on_account_id"
   end
 
+  create_table "identities", id: :serial, force: :cascade do |t|
+    t.integer "user_id"
+    t.string "provider", default: "", null: false
+    t.string "uid", default: "", null: false
+    t.datetime "created_at", null: false
+    t.datetime "updated_at", null: false
+    t.index ["user_id"], name: "index_identities_on_user_id"
+  end
+
   create_table "imports", force: :cascade do |t|
     t.integer "type", null: false
     t.boolean "approved", default: false, null: false
@@ -536,7 +545,11 @@ ActiveRecord::Schema.define(version: 20180109143959) do
   add_foreign_key "follow_requests", "accounts", name: "fk_76d644b0e7", on_delete: :cascade
   add_foreign_key "follows", "accounts", column: "target_account_id", name: "fk_745ca29eac", on_delete: :cascade
   add_foreign_key "follows", "accounts", name: "fk_32ed1b5560", on_delete: :cascade
+<<<<<<< HEAD
   add_foreign_key "glitch_keyword_mutes", "accounts", on_delete: :cascade
+=======
+  add_foreign_key "identities", "users", on_delete: :cascade
+>>>>>>> origin/master
   add_foreign_key "imports", "accounts", name: "fk_6db1b6e408", on_delete: :cascade
   add_foreign_key "invites", "users", on_delete: :cascade
   add_foreign_key "list_accounts", "accounts", on_delete: :cascade
diff --git a/spec/fabricators/identity_fabricator.rb b/spec/fabricators/identity_fabricator.rb
new file mode 100644
index 000000000..bc832df9f
--- /dev/null
+++ b/spec/fabricators/identity_fabricator.rb
@@ -0,0 +1,5 @@
+Fabricator(:identity) do
+  user     nil
+  provider "MyString"
+  uid      "MyString"
+end
diff --git a/spec/models/identity_spec.rb b/spec/models/identity_spec.rb
new file mode 100644
index 000000000..53f355410
--- /dev/null
+++ b/spec/models/identity_spec.rb
@@ -0,0 +1,5 @@
+require 'rails_helper'
+
+RSpec.describe Identity, type: :model do
+  pending "add some examples to (or delete) #{__FILE__}"
+end