Add support for SSO login.
authorjamesjulich <51384945+jamesjulich@users.noreply.github.com>
Sun, 10 Oct 2021 21:36:44 +0000 (16:36 -0500)
committerKrishan <33421343+kfiven@users.noreply.github.com>
Mon, 25 Oct 2021 12:29:57 +0000 (17:59 +0530)
src/app/molecules/sso-buttons/SSOButtons.jsx [new file with mode: 0644]
src/app/molecules/sso-buttons/SSOButtons.scss [new file with mode: 0644]
src/app/templates/auth/Auth.jsx
src/client/action/auth.js

diff --git a/src/app/molecules/sso-buttons/SSOButtons.jsx b/src/app/molecules/sso-buttons/SSOButtons.jsx
new file mode 100644 (file)
index 0000000..82e8cc4
--- /dev/null
@@ -0,0 +1,98 @@
+import React, { useEffect, useState } from 'react';
+import PropTypes from 'prop-types';
+import './SSOButtons.scss';
+
+import { createTemporaryClient, getLoginFlows, startSsoLogin } from '../../../client/action/auth';
+
+function SSOButtons({ homeserver }) {
+  const [identityProviders, setIdentityProviders] = useState([]);
+
+  useEffect(() => {
+    // If the homeserver passed in is not a fully-qualified domain name, do not update.
+    if (!homeserver.match('(?=^.{4,253}$)(^((?!-)[a-zA-Z0-9-]{1,63}(?<!-).)+[a-zA-Z]{2,63}$)')) {
+      return;
+    }
+
+    // TODO Check that there is a Matrix server at homename before making requests.
+    // This will prevent the CORS errors that happen when a user changes their homeserver.
+    createTemporaryClient(homeserver).then((client) => {
+      const providers = [];
+      getLoginFlows(client).then((flows) => {
+        if (flows.flows !== undefined) {
+          const ssoFlows = flows.flows.filter((flow) => flow.type === 'm.login.sso' || flow.type === 'm.login.cas');
+          ssoFlows.forEach((flow) => {
+            if (flow.identity_providers !== undefined) {
+              const type = flow.type.substring(8);
+              flow.identity_providers.forEach((idp) => {
+                const imageSrc = client.mxcUrlToHttp(idp.icon);
+                providers.push({
+                  homeserver, id: idp.id, name: idp.name, type, imageSrc,
+                });
+              });
+            }
+          });
+        }
+        setIdentityProviders(providers);
+      }).catch(() => {});
+    }).catch(() => {
+      setIdentityProviders([]);
+    });
+  }, [homeserver]);
+
+  // TODO Render all non-icon providers at the end so that they are never inbetween icons.
+  return (
+    <div className="sso-buttons">
+      {identityProviders.map((idp) => {
+        if (idp.imageSrc == null || idp.imageSrc === undefined || idp.imageSrc === '') {
+          return (
+            <button
+              key={idp.id}
+              type="button"
+              onClick={() => { startSsoLogin(homeserver, idp.type, idp.id); }}
+              className="sso-buttons__fallback-text text-b1"
+            >
+              {`Log in with ${idp.name}`}
+            </button>
+          );
+        }
+        return (
+          <SSOButton
+            key={idp.id}
+            homeserver={idp.homeserver}
+            id={idp.id}
+            name={idp.name}
+            type={idp.type}
+            imageSrc={idp.imageSrc}
+          />
+        );
+      })}
+    </div>
+  );
+}
+
+function SSOButton({
+  homeserver, id, name, type, imageSrc,
+}) {
+  function handleClick() {
+    startSsoLogin(homeserver, type, id);
+  }
+  return (
+    <button type="button" className="sso-button" onClick={handleClick}>
+      <img className="sso-button__img" src={imageSrc} alt={name} />
+    </button>
+  );
+}
+
+SSOButtons.propTypes = {
+  homeserver: PropTypes.string.isRequired,
+};
+
+SSOButton.propTypes = {
+  homeserver: PropTypes.string.isRequired,
+  id: PropTypes.string.isRequired,
+  name: PropTypes.string.isRequired,
+  type: PropTypes.string.isRequired,
+  imageSrc: PropTypes.string.isRequired,
+};
+
+export default SSOButtons;
diff --git a/src/app/molecules/sso-buttons/SSOButtons.scss b/src/app/molecules/sso-buttons/SSOButtons.scss
new file mode 100644 (file)
index 0000000..61d48bc
--- /dev/null
@@ -0,0 +1,31 @@
+.sso-buttons {
+  margin-top: var(--sp-extra-loose);
+
+  display: flex;
+  justify-content: center;
+  flex-wrap: wrap;
+
+  &__fallback-text {
+    margin: var(--sp-tight) 0px;
+
+    flex-basis: 100%;
+    text-align: center;
+
+    color: var(--bg-primary);
+    cursor: pointer;
+  }
+}
+
+.sso-button {
+  margin-bottom: var(--sp-normal);
+
+  display: flex;
+  justify-content: center;
+  flex-basis: 20%;
+
+  cursor: pointer;
+
+  &__img {
+    height: var(--av-normal);
+  }
+}
\ No newline at end of file
index 3d97ca519654be5272993ca3bfad84a483f789b1..3eaf16a89b4b6a6f0df314b193b623cf88aacf82 100644 (file)
@@ -3,8 +3,9 @@ import PropTypes from 'prop-types';
 import './Auth.scss';
 import ReCAPTCHA from 'react-google-recaptcha';
 
-import { Link } from 'react-router-dom';
+import { Link, useLocation } from 'react-router-dom';
 import * as auth from '../../../client/action/auth';
+import cons from '../../../client/state/cons';
 
 import Text from '../../atoms/text/Text';
 import Button from '../../atoms/button/Button';
@@ -15,6 +16,7 @@ import ScrollView from '../../atoms/scroll/ScrollView';
 
 import EyeIC from '../../../../public/res/ic/outlined/eye.svg';
 import CinnySvg from '../../../../public/res/svg/cinny.svg';
+import SSOButtons from '../../molecules/sso-buttons/SSOButtons';
 
 // This regex validates historical usernames, which don't satisfy today's username requirements.
 // See https://matrix.org/docs/spec/appendices#id13 for more info.
@@ -75,12 +77,35 @@ function normalizeUsername(rawUsername) {
 
 function Auth({ type }) {
   const [process, changeProcess] = useState(null);
+  const [homeserver, changeHomeserver] = useState('matrix.org');
+
   const usernameRef = useRef(null);
   const homeserverRef = useRef(null);
   const passwordRef = useRef(null);
   const confirmPasswordRef = useRef(null);
   const emailRef = useRef(null);
 
+  const { search } = useLocation();
+  const searchParams = new URLSearchParams(search);
+  if (searchParams.has('loginToken')) {
+    const loginToken = searchParams.get('loginToken');
+    if (loginToken !== undefined) {
+      if (localStorage.getItem(cons.secretKey.BASE_URL) !== undefined) {
+        const baseUrl = localStorage.getItem(cons.secretKey.BASE_URL);
+        auth.loginWithToken(baseUrl, loginToken)
+          .then(() => {
+            window.location.replace('/');
+          })
+          .catch((error) => {
+            changeProcess(null);
+            if (!error.contains('CORS request rejected')) {
+              renderErrorMessage(error);
+            }
+          });
+      }
+    }
+  }
+
   function register(recaptchaValue, terms, verified) {
     auth.register(
       usernameRef.current.value,
@@ -205,6 +230,7 @@ function Auth({ type }) {
               />
               <Input
                 forwardRef={homeserverRef}
+                onChange={(e) => changeHomeserver(e.target.value)}
                 id="auth_homeserver"
                 placeholder="Homeserver"
                 value="matrix.org"
@@ -281,6 +307,9 @@ function Auth({ type }) {
                 {type === 'login' ? 'Login' : 'Register' }
               </Button>
             </div>
+            {type === 'login' && (
+              <SSOButtons homeserver={homeserver} />
+            )}
           </form>
         </div>
 
index 6c77aa81ff9ab7955718f1edf17c35da888962c4..47fe2ba2615500dea85793f781f2a2d64ba59d22 100644 (file)
@@ -2,7 +2,8 @@ import * as sdk from 'matrix-js-sdk';
 import cons from '../state/cons';
 import { getBaseUrl } from '../../util/matrixUtil';
 
-async function login(username, homeserver, password) {
+// This method inspired by a similar one in matrix-react-sdk
+async function createTemporaryClient(homeserver) {
   let baseUrl = null;
   try {
     baseUrl = await getBaseUrl(homeserver);
@@ -12,7 +13,25 @@ async function login(username, homeserver, password) {
 
   if (typeof baseUrl === 'undefined') throw new Error('Homeserver not found');
 
-  const client = sdk.createClient({ baseUrl });
+  return sdk.createClient({ baseUrl });
+}
+
+async function getLoginFlows(client) {
+  const flows = await client.loginFlows();
+  if (flows !== undefined) {
+    return flows;
+  }
+  return null;
+}
+
+async function startSsoLogin(homeserver, type, idpId) {
+  const client = await createTemporaryClient(homeserver);
+  localStorage.setItem(cons.secretKey.BASE_URL, client.baseUrl);
+  window.location.href = client.getSsoLoginUrl(window.location.href, type, idpId);
+}
+
+async function login(username, homeserver, password) {
+  const client = await createTemporaryClient(homeserver);
 
   const response = await client.login('m.login.password', {
     identifier: {
@@ -26,7 +45,21 @@ async function login(username, homeserver, password) {
   localStorage.setItem(cons.secretKey.ACCESS_TOKEN, response.access_token);
   localStorage.setItem(cons.secretKey.DEVICE_ID, response.device_id);
   localStorage.setItem(cons.secretKey.USER_ID, response.user_id);
-  localStorage.setItem(cons.secretKey.BASE_URL, response?.well_known?.['m.homeserver']?.base_url || baseUrl);
+  localStorage.setItem(cons.secretKey.BASE_URL, response?.well_known?.['m.homeserver']?.base_url || client.baseUrl);
+}
+
+async function loginWithToken(baseUrl, token) {
+  const client = sdk.createClient(baseUrl);
+
+  const response = await client.login('m.login.token', {
+    token,
+    initial_device_display_name: cons.DEVICE_DISPLAY_NAME,
+  });
+
+  localStorage.setItem(cons.secretKey.ACCESS_TOKEN, response.access_token);
+  localStorage.setItem(cons.secretKey.DEVICE_ID, response.device_id);
+  localStorage.setItem(cons.secretKey.USER_ID, response.user_id);
+  localStorage.setItem(cons.secretKey.BASE_URL, response?.well_known?.['m.homeserver']?.base_url || client.baseUrl);
 }
 
 async function getAdditionalInfo(baseUrl, content) {
@@ -45,6 +78,7 @@ async function getAdditionalInfo(baseUrl, content) {
     throw new Error(e);
   }
 }
+
 async function verifyEmail(baseUrl, content) {
   try {
     const res = await fetch(`${baseUrl}/_matrix/client/r0/register/email/requestToken`, {
@@ -149,4 +183,7 @@ async function register(username, homeserver, password, email, recaptchaValue, t
   return {};
 }
 
-export { login, register };
+export {
+  createTemporaryClient, getLoginFlows, login,
+  loginWithToken, register, startSsoLogin,
+};