Use OAuth2 to login on firefox-ci Taskcluster instance (#160)

This commit is contained in:
Bastien Abadie 2020-01-16 19:07:09 +01:00 коммит произвёл GitHub
Родитель 53302b7035
Коммит d00763e8bd
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
12 изменённых файлов: 233 добавлений и 142 удалений

Просмотреть файл

@ -101,7 +101,7 @@ The authentication configuration has the following characteristics:
* There are two different Auth0 clients
* An official one (SSO + LDAP) and the other for non-LDAP contributors
* Non-LDAP users will receive fake org data
* After a user authenticates, the auth will also authenticate with Taskcluster (`login.taskcluster.net`)
* After a user authenticates, the auth will also authenticate with Firefox CI Taskcluster (`firefox-ci-tc.services.mozilla.com`)
* This is in order to later fetch a Taskcluster secret (only available to LDAP users)
## Running & tests

Просмотреть файл

@ -29,9 +29,11 @@
"@mozilla-frontend-infra/components": "^2.0.0",
"auth0-js": "9.6.1",
"chart.js": "^2.7.3",
"client-oauth2": "^4.2.5",
"mitt": "^1.1.3",
"moment": "^2.23.0",
"mui-datatables": "^2.4.0",
"pako": "^1.0.10",
"prop-types": "^15",
"query-string": "^6.2.0",
"react": "^16",
@ -41,8 +43,7 @@
"react-router-dom": "^4.3.1",
"taskcluster-client-web": "9.0.0",
"taskcluster-lib-urls": "^12.0.0",
"typeface-roboto": "^0.0.54",
"pako": "^1.0.10"
"typeface-roboto": "^0.0.54"
},
"devDependencies": {
"@neutrinojs/airbnb": "^9.0.0-rc.1",

Просмотреть файл

@ -16,8 +16,7 @@ import PropsRoute from '../components/PropsRoute';
import AuthContext from '../components/auth/AuthContext';
import AuthController from '../components/auth/AuthController';
import NotFound from '../components/NotFound';
import Auth0Login from '../views/Auth0Login';
import config from '../config';
import OAuth2Login from '../views/OAuth2Login';
const styles = () => ({
'@global': {
@ -67,9 +66,15 @@ class App extends React.Component {
this.handleUserSessionChanged,
);
// Start the Oauth code exchange when it hass received as /?code=XXX
const params = new URLSearchParams(window.location.search);
if (params.get('code') !== null) {
this.authController.exchangeCode(window.location.href);
}
// we do not want to automatically load a user session on the login views; this is
// a hack until they get an entry point of their own with no UI.
if (!window.location.pathname.startsWith(config.redirectRoute)) {
if (!window.location.pathname.startsWith('/login')) {
this.authController.loadUserSession();
} else {
this.setState({ authReady: true });
@ -91,8 +96,8 @@ class App extends React.Component {
<Redirect to="/reportees" />
</Route>
<PropsRoute
path={config.redirectRoute}
component={Auth0Login}
path="/login"
component={OAuth2Login}
setUserSession={this.authController.setUserSession}
/>
<PropsRoute path="/" component={Main} />

Просмотреть файл

@ -1,7 +1,7 @@
import mitt from 'mitt';
import UserSession from './UserSession';
import { renew as auth0Renew } from './auth0';
import { userSessionFromCode, renew as auth0Renew } from './oauth2';
/**
* Controller for authentication-related pieces of the site.
@ -53,6 +53,14 @@ export default class AuthController {
}
}
/**
* Exchange Oauth Code received in URL callback
* and build a User Session from Taskcluster credentials
*/
async exchangeCode(url) {
this.setUserSession(await userSessionFromCode(url));
}
/**
* Load the current user session (from localStorage).
*

Просмотреть файл

@ -1,7 +1,6 @@
import { OIDCCredentialAgent, Secrets, Index } from 'taskcluster-client-web';
import { withRootUrl } from 'taskcluster-lib-urls';
import { Secrets, Index } from 'taskcluster-client-web';
import { TASKCLUSTER_ROOT_URL } from '../../config';
const urls = withRootUrl('https://taskcluster.net');
/**
* An object representing a user session. Tools supports a variety of login methods,
* so this combines them all in a single representation.
@ -18,14 +17,6 @@ const urls = withRootUrl('https://taskcluster.net');
* - renewAfter - date (Date or string) after which this session should be renewed,
* if applicable
*
* When type is 'oidc':
*
* - oidcProvider -- the provider (see taskcluster-login)
* - accessToken -- the accessToken to pass to taskcluster-login
* - fullName -- user's full name
* - picture -- URL of an image of the user
* - oidcSubject -- the 'sub' field of the id_token (useful for debugging user issues)
*
* When the type is 'credentials':
*
* - credentials -- the Taskcluster credentials (with or without a certificate)
@ -36,21 +27,10 @@ const urls = withRootUrl('https://taskcluster.net');
export default class UserSession {
constructor(options) {
Object.assign(this, options);
if (this.accessToken) {
this.credentialAgent = new OIDCCredentialAgent({
accessToken: this.accessToken,
url: urls.api('login', 'v1', `/oidc-credentials/${this.oidcProvider}`),
});
}
}
static fromCredentials(credentials) {
return new UserSession({ type: 'credentials', credentials });
}
static fromOIDC(options) {
return new UserSession({ type: 'oidc', ...options });
return new UserSession({ type: 'credentials', email: 'nobody@mozilla.org', credentials });
}
// determine whether the user changed from old to new; this is used by other components
@ -78,9 +58,7 @@ export default class UserSession {
// get the args used to create a new client object
get clientArgs() {
return this.credentialAgent
? { credentialAgent: this.credentialAgent }
: { credentials: this.credentials };
return { credentials: this.credentials };
}
// load Taskcluster credentials for this user
@ -98,7 +76,13 @@ export default class UserSession {
return JSON.stringify({ ...this, credentialAgent: undefined });
}
getTaskClusterSecretsClient = () => new Secrets({ ...this.clientArgs, rootUrl: 'https://taskcluster.net' });
getTaskClusterSecretsClient = () => new Secrets({
...this.clientArgs,
rootUrl: TASKCLUSTER_ROOT_URL,
});
getTaskClusterIndexClient = () => new Index({ ...this.clientArgs, rootUrl: 'https://taskcluster.net' });
getTaskClusterIndexClient = () => new Index({
...this.clientArgs,
rootUrl: TASKCLUSTER_ROOT_URL,
});
}

Просмотреть файл

@ -1,40 +0,0 @@
import { fromNow } from 'taskcluster-client-web';
import { WebAuth } from 'auth0-js';
import UserSession from './UserSession';
import config from '../../config';
export const webAuth = new WebAuth(config.auth0Options);
export function userSessionFromAuthResult(authResult) {
return UserSession.fromOIDC({
oidcProvider: 'mozilla-auth0',
accessToken: authResult.accessToken,
fullName: authResult.idTokenPayload.name === '' ? authResult.idTokenPayload.nickname : authResult.idTokenPayload.name,
email: authResult.idTokenPayload.email,
picture: authResult.idTokenPayload.picture,
oidcSubject: authResult.idTokenPayload.sub,
// per https://wiki.mozilla.org/Security/Guidelines/OpenID_connect#Session_handling
renewAfter: fromNow('15 minutes'),
});
}
/* eslint-disable consistent-return */
export async function renew({ userSession, authController }) {
if (
!userSession
|| userSession.type !== 'oidc'
|| userSession.oidcProvider !== 'mozilla-auth0'
) {
return;
}
return new Promise((accept, reject) => webAuth.renewAuth({}, (err, authResult) => {
if (err) {
return reject(err);
} if (!authResult) {
return reject(new Error('no authResult'));
}
authController.setUserSession(userSessionFromAuthResult(authResult));
accept();
}));
}

Просмотреть файл

@ -0,0 +1,47 @@
import ClientOAuth2 from 'client-oauth2';
import UserSession from './UserSession';
import config from '../../config';
export const webAuth = new ClientOAuth2(config.OAuth2Options);
// Part 1 - Redirect the user on Taskcluster instance to start the OAuth2 flow
export function redirectUser() {
window.location.href = webAuth.code.getUri();
}
// Part 2 - Exchange Oauth code for Taskcluster credentials
export async function userSessionFromCode(url) {
// Get Oauth access token
const user = await webAuth.code.getToken(url);
// Exchange that access token for some Taskcluster credentials
const request = user.sign({
method: 'get',
});
const resp = await fetch(config.OAuth2Options.credentialsUri, request);
const payload = await resp.json();
// Finally build a new user session
return UserSession.fromCredentials(payload.credentials);
}
/* eslint-disable consistent-return */
export async function renew({ userSession }) {
if (
!userSession
|| userSession.type !== 'oidc'
|| userSession.oidcProvider !== 'mozilla-auth0'
) {
return;
}
return new Promise((accept, reject) => webAuth.renewAuth({}, (err, authResult) => {
if (err) {
return reject(err);
} if (!authResult) {
return reject(new Error('no authResult'));
}
// TODO: somehow renew ? Not possible i think with Oauth + TC
accept();
}));
}

Просмотреть файл

@ -1,21 +1,24 @@
const loginCallbackRoute = '/callback';
const PRODUCTION = !(process.env.NODE_ENV === 'production');
export const TASKCLUSTER_ROOT_URL = PRODUCTION ? 'https://firefox-ci-tc.services.mozilla.com' : 'https://stage.taskcluster.nonprod.cloudops.mozgcp.net';
const config = {
redirectRoute: loginCallbackRoute,
artifactRoute: 'project.relman.testing.bugzilla-dashboard.latest',
artifactRoute: 'project.relman.production.bugzilla-dashboard.latest',
taskclusterSecrets: {
orgData: 'project/bugzilla-management-dashboard/realOrg',
},
auth0Options: {
domain: process.env.ALTERNATIVE_AUTH ? 'mozilla-frontend-infra.auth0.com' : 'auth.mozilla.auth0.com',
clientID: process.env.ALTERNATIVE_AUTH ? 'nWIQUJ5lOiyYHgK4Jm5nPs5hM6JUizwt' : 'DGloMN2BXb0AC7lF5eRyOe1GXweqBAiI',
redirectUri: new URL(loginCallbackRoute, window.location).href,
scope: 'taskcluster-credentials full-user-credentials openid profile email',
audience: process.env.ALTERNATIVE_AUTH ? '' : 'login.taskcluster.net',
responseType: 'token id_token',
OAuth2Options: {
clientId: PRODUCTION ? 'bugzilla-dashboard-production' : 'bugzilla-dashboard-localdev',
scopes: ['queue:get-artifact:project/relman/bugzilla-dashboard/*'],
authorizationUri: `${TASKCLUSTER_ROOT_URL}/login/oauth/authorize`,
accessTokenUri: `${TASKCLUSTER_ROOT_URL}/login/oauth/token`,
credentialsUri: `${TASKCLUSTER_ROOT_URL}/login/oauth/credentials`,
redirectUri: PRODUCTION ? 'https://bugzilla-management-dashboard.netlify.com' : 'http://localhost:5000',
whitelisted: true,
responseType: 'code',
maxExpires: '15 minutes',
},
productComponentMetrics: 'private/bugzilla-dashboard/product_component_data.json.gz',
reporteesMetrics: 'private/bugzilla-dashboard/reportee_data.json.gz',
productComponentMetrics: 'project/relman/bugzilla-dashboard/product_component_data.json.gz',
reporteesMetrics: 'project/relman/bugzilla-dashboard/reportee_data.json.gz',
};
export const REPORTEES_CONFIG = {

Просмотреть файл

@ -1,51 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import ErrorPanel from '@mozilla-frontend-infra/components/ErrorPanel';
import { webAuth, userSessionFromAuthResult } from '../../components/auth/auth0';
export default class Auth0Login extends React.PureComponent {
static propTypes = {
history: PropTypes.shape({}).isRequired,
setUserSession: PropTypes.func.isRequired,
};
state = {};
componentDidMount() {
const { history, setUserSession } = this.props;
if (!window.location.hash) {
webAuth.authorize();
} else if (window !== window.top) {
// for silent renewal, auth0-js opens this page in an iframe, and expects
// a postMessage back, and that's it.
window.parent.postMessage(window.location.hash, window.origin);
} else {
webAuth.parseHash(window.location.hash, (loginError, authResult) => {
if (loginError) {
this.setState({ loginError });
} else {
setUserSession(userSessionFromAuthResult(authResult));
if (window.opener) {
window.close();
} else {
history.push('/');
}
}
});
}
}
render() {
const { loginError } = this.state;
if (loginError) {
return <ErrorPanel error={loginError} />;
}
if (window.location.hash) {
return <p>Logging in..</p>;
}
return <p>Redirecting..</p>;
}
}

Просмотреть файл

@ -4,7 +4,6 @@ import { withStyles } from '@material-ui/core/styles';
import Button from '@material-ui/core/Button';
import AuthContext from '../../components/auth/AuthContext';
import config from '../../config';
import ProfileMenu from '../ProfileMenu';
const styles = theme => ({
@ -21,7 +20,7 @@ class CredentialsMenu extends React.PureComponent {
};
static handleLoginRequest() {
const loginView = new URL(config.redirectRoute, window.location);
const loginView = new URL('/login', window.location);
window.open(loginView, '_blank');
}

Просмотреть файл

@ -0,0 +1,27 @@
import React from 'react';
import ErrorPanel from '@mozilla-frontend-infra/components/ErrorPanel';
import { redirectUser } from '../../components/auth/oauth2';
export default class OAuth2Login extends React.PureComponent {
state = {};
componentDidMount() {
if (!window.location.hash) {
// Start login flow
redirectUser();
}
}
render() {
const { loginError } = this.state;
if (loginError) {
return <ErrorPanel error={loginError} />;
}
if (window.location.hash) {
return <p>Logging in..</p>;
}
return <p>Redirecting..</p>;
}
}

108
yarn.lock
Просмотреть файл

@ -1095,6 +1095,11 @@
dependencies:
any-observable "^0.3.0"
"@servie/events@^1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@servie/events/-/events-1.0.0.tgz#8258684b52d418ab7b86533e861186638ecc5dc1"
integrity sha512-sBSO19KzdrJCM3gdx6eIxV8M9Gxfgg6iDQmH5TIAGaUu+X9VDdsINXJOnoiZ1Kx3TrHdH4bt5UVglkjsEGBcvw==
"@types/babel__core@^7.1.0":
version "7.1.1"
resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.1.tgz#ce9a9e5d92b7031421e1d0d74ae59f572ba48be6"
@ -1190,6 +1195,11 @@
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e"
integrity sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw==
"@types/tough-cookie@^2.3.5":
version "2.3.6"
resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-2.3.6.tgz#c880579e087d7a0db13777ff8af689f4ffc7b0d5"
integrity sha512-wHNBMnkoEBiRAd3s8KTKwIuO9biFtTf0LehITzBhSco+HQI0xkXZbLOD55SW3Aqw3oUkHstkm5SPv58yaAdFPQ==
"@types/yargs@^12.0.2", "@types/yargs@^12.0.9":
version "12.0.12"
resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-12.0.12.tgz#45dd1d0638e8c8f153e87d296907659296873916"
@ -2051,6 +2061,11 @@ builtin-status-codes@^3.0.0:
resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8"
integrity sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=
byte-length@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/byte-length/-/byte-length-1.0.2.tgz#ba5a5909240b0121c079b7f7b15248d6f08223cc"
integrity sha512-ovBpjmsgd/teRmgcPh23d4gJvxDoXtAzEL9xTfMU8Yc2kqCDb7L9jAG0XHl1nzuGl+h3ebCIF1i62UFyA9V/2Q==
bytes@3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048"
@ -2326,6 +2341,14 @@ cli-width@^2.0.0:
resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.0.tgz#ff19ede8a9a5e579324147b0c11f0fbcbabed639"
integrity sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=
client-oauth2@^4.2.5:
version "4.2.5"
resolved "https://registry.yarnpkg.com/client-oauth2/-/client-oauth2-4.2.5.tgz#cee9499ef0acc84ee545a76a8a51942ddf26f473"
integrity sha512-GAhVLveAbBkwcfEH/d5lTW9eCgcPR3Up93cx7v4qWTdLCa4O0m3ykNNn4aAVeWOiHfWL5skO+3u0F/gfAxZuPQ==
dependencies:
popsicle "12.0.4"
safe-buffer "^5.1.1"
cliui@^4.0.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/cliui/-/cliui-4.1.0.tgz#348422dbe82d800b3022eef4f6ac10bf2e4d1b49"
@ -5923,6 +5946,18 @@ make-dir@^2.0.0, make-dir@^2.1.0:
pify "^4.0.1"
semver "^5.6.0"
make-error-cause@^2.2.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/make-error-cause/-/make-error-cause-2.3.0.tgz#ecd11875971e506d510e93d37796e5b83f46d6f9"
integrity sha512-etgt+n4LlOkGSJbBTV9VROHA5R7ekIPS4vfh+bCAoJgRrJWdqJCBbpS3osRJ/HrT7R68MzMiY3L3sDJ/Fd8aBg==
dependencies:
make-error "^1.3.5"
make-error@^1.3.5:
version "1.3.5"
resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.5.tgz#efe4e81f6db28cadd605c70f29c831b58ef776c8"
integrity sha512-c3sIjNUow0+8swNwVpqoH4YCShKNFkMaw6oH1mNS2haDZQqkeZFlHS3dhoeEbKKmJB4vXpJucU6oH75aDYeE9g==
makeerror@1.0.x:
version "1.0.11"
resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.11.tgz#e01a5c9109f2af79660e4e8b9587790184f5a96c"
@ -7061,6 +7096,57 @@ popper.js@^1.14.1:
resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.15.0.tgz#5560b99bbad7647e9faa475c6b8056621f5a4ff2"
integrity sha512-w010cY1oCUmI+9KwwlWki+r5jxKfTFDVoadl7MSrIujHU5MJ5OR6HTDj6Xo8aoR/QsA56x8jKjA59qGH4ELtrA==
popsicle-content-encoding@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/popsicle-content-encoding/-/popsicle-content-encoding-1.0.0.tgz#2ab419083fee0387bf6e64d21b1a9af560795adb"
integrity sha512-4Df+vTfM8wCCJVTzPujiI6eOl3SiWQkcZg0AMrOkD1enMXsF3glIkFUZGvour1Sj7jOWCsNSEhBxpbbhclHhzw==
popsicle-cookie-jar@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/popsicle-cookie-jar/-/popsicle-cookie-jar-1.0.0.tgz#9e8c89be7182b31f7ce0e66dad465ae475d8f47c"
integrity sha512-vrlOGvNVELko0+J8NpGC5lHWDGrk8LQJq9nwAMIVEVBfN1Lib3BLxAaLRGDTuUnvl45j5N9dT2H85PULz6IjjQ==
dependencies:
"@types/tough-cookie" "^2.3.5"
tough-cookie "^3.0.1"
popsicle-redirects@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/popsicle-redirects/-/popsicle-redirects-1.1.0.tgz#2a5abb49a7ad49c02e90b24d4608dc0b8b23176a"
integrity sha512-XCpzVjVk7tty+IJnSdqWevmOr1n8HNDhL86v7mZ6T1JIIf2KGybxUk9mm7ZFOhWMkGB0e8XkacHip7BV8AQWQA==
popsicle-transport-http@^1.0.0:
version "1.0.6"
resolved "https://registry.yarnpkg.com/popsicle-transport-http/-/popsicle-transport-http-1.0.6.tgz#b73a65426f7ef9d0bfedd98673b84cd92e061bdd"
integrity sha512-J/d1MhlqgaDro9xWe31RCNFBlUs3kG52rl7YNKYZdF8nllgGtXwhfcLlzwNJOW/M+nPOyxFvqOZIi6Qq599Hlw==
dependencies:
make-error-cause "^2.2.0"
pump "^3.0.0"
popsicle-transport-xhr@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/popsicle-transport-xhr/-/popsicle-transport-xhr-1.0.2.tgz#aa4b7ab74d37f880cf857622cbbaf5ead3e43cb2"
integrity sha512-v9eAJnj1tydT4VmDdyKFE1z/+oL01vB7AS3LfSFMAYv33dzqlxtbApKALcYWBQotIqw3FoIqd2FiDR6qJsOxtA==
popsicle-user-agent@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/popsicle-user-agent/-/popsicle-user-agent-1.0.0.tgz#976af355b605966168733c4e03ad1e4f783f5d48"
integrity sha512-epKaq3TTfTzXcxBxjpoKYMcTTcAX8Rykus6QZu77XNhJuRHSRxMd+JJrbX/3PFI0opFGSN0BabbAYCbGxbu0mA==
popsicle@12.0.4:
version "12.0.4"
resolved "https://registry.yarnpkg.com/popsicle/-/popsicle-12.0.4.tgz#297adb1132a79fdbc54ca902645811b177f6234f"
integrity sha512-UuxhAFa4RXBecC6ZK24sKra/9va1bTxnb3CQpFsm+VBW72sl+UtTAmZv7LZTvvDNnGusAqisN+a6xSN9xSQzZA==
dependencies:
popsicle-content-encoding "^1.0.0"
popsicle-cookie-jar "^1.0.0"
popsicle-redirects "^1.0.0"
popsicle-transport-http "^1.0.0"
popsicle-transport-xhr "^1.0.0"
popsicle-user-agent "^1.0.0"
servie "^4.0.6"
throwback "^4.1.0"
tough-cookie "^3.0.1"
portfinder@^1.0.21:
version "1.0.21"
resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.21.tgz#60e1397b95ac170749db70034ece306b9a27e324"
@ -8044,6 +8130,14 @@ serve-static@1.14.1:
parseurl "~1.3.3"
send "0.17.1"
servie@^4.0.6:
version "4.3.2"
resolved "https://registry.yarnpkg.com/servie/-/servie-4.3.2.tgz#7168140d62cb9476cb8b184fc8ceda24d5154e7e"
integrity sha512-1NpFf3LjkDDq4IIuBqtqHfSdPWhXpuyWwuBdwbifZjWSxQd8rCWz5W9AluxNvWfteM1qQ26puODIzWljvBJc5A==
dependencies:
"@servie/events" "^1.0.0"
byte-length "^1.0.2"
set-blocking@^2.0.0, set-blocking@~2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
@ -8715,6 +8809,11 @@ through@^2.3.6, through@~2.3.6:
resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=
throwback@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/throwback/-/throwback-4.1.0.tgz#421aac7ba9eff473105385ac4a2b0130d4b0a59c"
integrity sha512-dLFe8bU8SeH0xeqeKL7BNo8XoPC/o91nz9/ooeplZPiso+DZukhoyZcSz9TFnUNScm+cA9qjU1m1853M6sPOng==
thunky@^1.0.2:
version "1.0.3"
resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.0.3.tgz#f5df732453407b09191dae73e2a8cc73f381a826"
@ -8802,6 +8901,15 @@ tough-cookie@^2.3.3, tough-cookie@^2.3.4:
psl "^1.1.28"
punycode "^2.1.1"
tough-cookie@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-3.0.1.tgz#9df4f57e739c26930a018184887f4adb7dca73b2"
integrity sha512-yQyJ0u4pZsv9D4clxO69OEjLWYw+jbgspjTue4lTQZLfV0c5l1VmK2y1JK8E9ahdpltPOaAThPcp5nKPUgSnsg==
dependencies:
ip-regex "^2.1.0"
psl "^1.1.28"
punycode "^2.1.1"
tough-cookie@~2.4.3:
version "2.4.3"
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.4.3.tgz#53f36da3f47783b0925afa06ff9f3b165280f781"