Add support for SSO login.
This commit is contained in:
parent
7b67e4a6e6
commit
8a1b749667
4 changed files with 200 additions and 5 deletions
98
src/app/molecules/sso-buttons/SSOButtons.jsx
Normal file
98
src/app/molecules/sso-buttons/SSOButtons.jsx
Normal file
|
@ -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;
|
31
src/app/molecules/sso-buttons/SSOButtons.scss
Normal file
31
src/app/molecules/sso-buttons/SSOButtons.scss
Normal file
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,8 +3,9 @@ import PropTypes from 'prop-types';
|
||||||
import './Auth.scss';
|
import './Auth.scss';
|
||||||
import ReCAPTCHA from 'react-google-recaptcha';
|
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 * as auth from '../../../client/action/auth';
|
||||||
|
import cons from '../../../client/state/cons';
|
||||||
|
|
||||||
import Text from '../../atoms/text/Text';
|
import Text from '../../atoms/text/Text';
|
||||||
import Button from '../../atoms/button/Button';
|
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 EyeIC from '../../../../public/res/ic/outlined/eye.svg';
|
||||||
import CinnySvg from '../../../../public/res/svg/cinny.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.
|
// This regex validates historical usernames, which don't satisfy today's username requirements.
|
||||||
// See https://matrix.org/docs/spec/appendices#id13 for more info.
|
// See https://matrix.org/docs/spec/appendices#id13 for more info.
|
||||||
|
@ -75,12 +77,35 @@ function normalizeUsername(rawUsername) {
|
||||||
|
|
||||||
function Auth({ type }) {
|
function Auth({ type }) {
|
||||||
const [process, changeProcess] = useState(null);
|
const [process, changeProcess] = useState(null);
|
||||||
|
const [homeserver, changeHomeserver] = useState('matrix.org');
|
||||||
|
|
||||||
const usernameRef = useRef(null);
|
const usernameRef = useRef(null);
|
||||||
const homeserverRef = useRef(null);
|
const homeserverRef = useRef(null);
|
||||||
const passwordRef = useRef(null);
|
const passwordRef = useRef(null);
|
||||||
const confirmPasswordRef = useRef(null);
|
const confirmPasswordRef = useRef(null);
|
||||||
const emailRef = 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) {
|
function register(recaptchaValue, terms, verified) {
|
||||||
auth.register(
|
auth.register(
|
||||||
usernameRef.current.value,
|
usernameRef.current.value,
|
||||||
|
@ -205,6 +230,7 @@ function Auth({ type }) {
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
forwardRef={homeserverRef}
|
forwardRef={homeserverRef}
|
||||||
|
onChange={(e) => changeHomeserver(e.target.value)}
|
||||||
id="auth_homeserver"
|
id="auth_homeserver"
|
||||||
placeholder="Homeserver"
|
placeholder="Homeserver"
|
||||||
value="matrix.org"
|
value="matrix.org"
|
||||||
|
@ -281,6 +307,9 @@ function Auth({ type }) {
|
||||||
{type === 'login' ? 'Login' : 'Register' }
|
{type === 'login' ? 'Login' : 'Register' }
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
{type === 'login' && (
|
||||||
|
<SSOButtons homeserver={homeserver} />
|
||||||
|
)}
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,8 @@ import * as sdk from 'matrix-js-sdk';
|
||||||
import cons from '../state/cons';
|
import cons from '../state/cons';
|
||||||
import { getBaseUrl } from '../../util/matrixUtil';
|
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;
|
let baseUrl = null;
|
||||||
try {
|
try {
|
||||||
baseUrl = await getBaseUrl(homeserver);
|
baseUrl = await getBaseUrl(homeserver);
|
||||||
|
@ -12,7 +13,25 @@ async function login(username, homeserver, password) {
|
||||||
|
|
||||||
if (typeof baseUrl === 'undefined') throw new Error('Homeserver not found');
|
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', {
|
const response = await client.login('m.login.password', {
|
||||||
identifier: {
|
identifier: {
|
||||||
|
@ -26,7 +45,21 @@ async function login(username, homeserver, password) {
|
||||||
localStorage.setItem(cons.secretKey.ACCESS_TOKEN, response.access_token);
|
localStorage.setItem(cons.secretKey.ACCESS_TOKEN, response.access_token);
|
||||||
localStorage.setItem(cons.secretKey.DEVICE_ID, response.device_id);
|
localStorage.setItem(cons.secretKey.DEVICE_ID, response.device_id);
|
||||||
localStorage.setItem(cons.secretKey.USER_ID, response.user_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) {
|
async function getAdditionalInfo(baseUrl, content) {
|
||||||
|
@ -45,6 +78,7 @@ async function getAdditionalInfo(baseUrl, content) {
|
||||||
throw new Error(e);
|
throw new Error(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function verifyEmail(baseUrl, content) {
|
async function verifyEmail(baseUrl, content) {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${baseUrl}/_matrix/client/r0/register/email/requestToken`, {
|
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 {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
export { login, register };
|
export {
|
||||||
|
createTemporaryClient, getLoginFlows, login,
|
||||||
|
loginWithToken, register, startSsoLogin,
|
||||||
|
};
|
||||||
|
|
Loading…
Reference in a new issue