File size: 5,924 Bytes
874ae95
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
module Users
  class OmniauthCallbacksController < Devise::OmniauthCallbacksController
    include Devise::Controllers::Rememberable

    skip_before_action :verify_authenticity_token, only: [:developer]

    # GET /users/auth/:action/callback
    def oauth_callback
      @email = email_from_auth_hash

      if oauth_origin.present? && oauth_origin[:session_id]
        if oauth_origin[:link_data].present?
          passthru_oauth_data
        elsif @email.blank?
          redirect_to(
            oauth_error_url(
              host: oauth_origin[:fqdn],
              error: email_blank_flash
            ),
            allow_other_host: true
          )
          nil
        else
          sign_in_at_oauth_origin
        end
      else
        render "oauth_origin_missing", layout: "error"
      end
    end

    alias google_oauth2 oauth_callback
    alias facebook oauth_callback
    alias github oauth_callback
    alias discord oauth_callback
    alias developer oauth_callback

    def failure
      if oauth_origin.present?
        message = t(".denied_by", provider: oauth_origin[:provider].capitalize)

        redirect_to(
          oauth_error_url(host: oauth_origin[:fqdn], error: message),
          allow_other_host: true
        )
      else
        flash[:error] = t(".denied")
        redirect_to new_user_session_path
      end
    end

    private

    def oauth_origin
      @oauth_origin ||=
        begin
          raw_origin_data = read_cookie(:oauth_origin)

          # Parse the JSON format that origin information is stored as.
          if raw_origin_data.present?
            # Make sure the cookie isn't reused.
            cookies.delete :oauth_origin

            JSON.parse(raw_origin_data, symbolize_names: true)
          end
        end
    end

    # This method is called when the user is already signed in, and is trying to link their social account with user.
    def passthru_oauth_data
      encrypted_token =
        EncryptorService.new.encrypt(
          { auth_hash: auth_hash_data, session_id: oauth_origin[:session_id] }
        )

      token_url_options = {
        encrypted_token: Base64.urlsafe_encode64(encrypted_token),
        host: oauth_origin[:fqdn]
      }

      redirect_to(
        user_auth_callback_url(**token_url_options),
        allow_other_host: true
      )
    end

    # This method is called when the user is not signed in, and is trying to sign in using OAuth.
    def sign_in_at_oauth_origin
      if user.present?
        user.regenerate_login_token

        encrypted_token =
          EncryptorService.new.encrypt(
            {
              login_token: user.original_login_token,
              auth_hash: auth_hash_data,
              session_id: oauth_origin[:session_id]
            }
          )

        token_url_options = {
          encrypted_token: Base64.urlsafe_encode64(encrypted_token),
          host: oauth_origin[:fqdn]
        }

        redirect_to(
          user_auth_callback_url(token_url_options),
          allow_other_host: true
        )
      else
        redirect_to(
          oauth_error_url(
            host: oauth_origin[:fqdn],
            error:
              t(
                "users.omniauth_callbacks.oauth_callback.email_unregistered",
                email: @email
              )
          ),
          allow_other_host: true
        )
      end
    end

    def user
      @user ||=
        begin
          school =
            School
              .joins(:domains)
              .where(domains: { fqdn: oauth_origin[:fqdn] })
              .first
          school.users.with_email(@email).first
        end
    end

    # This method is used to pass the auth_hash data to the oauth_origin.
    def auth_hash_data
      case auth_hash[:provider]
      when "google_oauth2", "facebook", "github", "developer"
        {}
      when "discord"
        {
          discord: {
            uid: auth_hash[:uid],
            tag:
              "#{auth_hash[:extra][:raw_info][:username]}##{auth_hash[:extra][:raw_info][:discriminator]}",
            access_token: auth_hash[:credentials][:token]
          }
        }
      else
        raise_unexpected_provider(provider)
      end
    end

    # This is a hack to resolve the issue of flashing message 'You are already signed in' when signing in using OAuth.
    # For an unknown reason, the request env variable omniauth.origin defaults to the sign in path when no origin is
    # supplied to the omniauth provider login path. This method detects and removes that default.
    def origin
      supplied_origin = request.env["omniauth.origin"]
      supplied_origin.include?('users/sign_in') ? nil : supplied_origin
    end

    # Omniauth returns authentication details in the 'omniauth.auth' request environment variable after the provider
    # redirects back to our website. The format for this return value is documented by Omniauth.
    def auth_hash
      request.env["omniauth.auth"]
    end

    # This method validates the format of auth_hash. This ensures that we capture any 'oddities' as crashes, instead of
    # letting issues get buried (we used to show a useless 404).
    def email_from_auth_hash
      raise "Auth hash is blank: #{auth_hash.inspect}" if auth_hash.blank?

      auth_hash.dig(:info, :email)
    end

    def provider_name
      params[:action].split("_").first.capitalize
    end

    def email_blank_flash
      message =
        t(
          "users.omniauth_callbacks.oauth_callback.not_receive_email",
          provider_name: provider_name
        )

      message +=
        case provider_name
        when "Github"
          t("users.omniauth_callbacks.oauth_callback.add_github")
        when "Facebook"
          t("users.omniauth_callbacks.oauth_callback.add_facebook")
        else
          t("users.omniauth_callbacks.oauth_callback.add_other")
        end

      message.html_safe
    end
  end
end